콘솔 응용 프로그램은 외형적으로 과거의 도스(DOS)와 닮아 도스를 위한 호환 모드라고 생각하기 쉽다. 하지만 콘솔 응용 프로그램도 표준 윈도우 응용 프로그램의 한 형태다. 그런 의미에서 '도스 창'은 잘못된 표현이다.
콘솔 응용 프로그램은 비교적 작성하기 쉬워 교육용으로 적합하다. 어떤 프로그램은 콘솔 응용 프로그램으로 작성해 독립적인 기능을 하기도 하며(ex. 윈도우의 ipconfig.exe. telnet.exe, ftp.exe, ping.exe, tracert.exe 등), 규모가 큰 프로그램의 한 모듈로 사용하기도 한다. 또한 GUI 응용 프로그램에서 콘솔 창을 따로 만들어 디버깅 정보를 출력하는 목적으로 사용한다.
유용한 기능을 하는 콘솔 응용 프로그램을 작성하고 이를 다른 프로그램에서 사용하고자 한다면 CreateProcess()
API 함수를 사용하면 된다. CreateProcess()
함수는 문자열 형태로 전달된 실행 파일을 찾아 새로운 프로세스를 생성한다.
STARTUPINFO si;
PROCESS_INFOMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
if (!CreateProcess(NULL, "실행 파일 이름", NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
오류 출력;
}
이러면 콘솔 창이 화면에 뜨는데, 때로는 창을 숨기고 싶을 때가 있다. 이럴 때는 다음과 같이 코드를 수정한다.
STARTUPINFO si;
PROCESS_INFOMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
if (!CreateProcess(NULL, "실행 파일 이름", NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi))
{
오류 출력;
}
한 프로세스 안에 존재하는 모든 스레드는 코드, 데이터(전역, 정적 변수), 힙, 환경 변수를 공유한다. 반면 스택(함수 전달 인자, 지역 변수)은 스레드별로 할당되며, 스레드가 실행 중에 사용하는 CPU 레지스터 값도 운영체제가 스레드별로 독립적으로 유지/관리**해준다.
C/C++로 응용 프로그램을 개발할 때는 함수 단위로 기능을 분할하는 편인데, 멀티스레드에서 안심하고 호출하려면 지역 변수만 사용해서 구현하는 것이 좋다. 앞서 언급한 것처럼 지역 변수, 즉 스택은 스레드별로 존재하므로 별도의 스레드 동기화 기법을 사용해 보호하지 않아도 안전하기 때문이다. 하지만 전역 변수나 정적 변수를 1개 이상 사용해서 함수를 구현하는 경우에는 조심해야 한다. 전역 변수나 정적 변수의 사용 목적이 데이터 공유라면 적절한 스레드 동기화 기법을 사용해 보호하면 되지만, 그 외에는 멀티스레드 환경에서 문제를 일으킬 가능성만 늘어난다.
구현상의 이유로 전역 변수나 정적 변수를 함수에서 사용하되 데이터 공유가 목적이 아니라면 스레드 지역 저장소를 사용하면 된다. 스레드 지역 저장소(TLS: Thread Local Storage)는 스레드별로 전역 변수나 정적 변수 등을 위한 저장 공간이 필요할 때 윈도우 운영체제가 할당해주는 메모리 영역이다. 스레드 지역 저장소는 윈도우 API 함수(TlsAlloc()
, TlsSetValue()
, TlsGetValue()
, TlsFree()
) 수준에서 지원되지만, 비주얼 C++의 확장 기능을 통해 좀 더 편리하게 사용할 수 있다.
아래 코드를 보면 TestFunc() 함수가 전역 변수, 정적 변수, 지역 변수를 사용하고 있다. 둘 이상의 스레드가 TestFunc() 함수를 동시에 호출해 사용하면 전역 변수와 정적 변수를 원치않게 공유하기 되어 문제가 발생할 수 있다.
int glo_var; // 전역 변수
void TestFunc()
{
static int sta_var; // 정적 변수
int loc_var; // 지역 변수
glo_var 변수 사용;
sta_var 변수 사용;
loc_var 변수 사용;
}
다음 아래 코드처럼 전역 변수와 정적 변수 앞에 __declspec(thread)
지시자를 붙이면 문제가 해결된다. 이제 전역 변수와 정적 변수는 프로그세스 내에 존재하는 스레드 개수만큼 존재하게 되어 각 스레드가 자신만의 전역 변수와 정적 변수를 가지기 때문에 불필요한 메모리 영역 공유가 생기지 않는다. 참고로, 지역 변수는 스레드별로 스택 영역에 따로 할당되므로 스레드 지역 저장소를 사용할 필요가 없다.
__declspec(thread) int glo_var; // 전역 변수
void TestFunc()
{
__declspec(thread) static int sta_var; // 정적 변수
int loc_var; // 지역 변수
glo_var 변수 사용;
sta_var 변수 사용;
loc_var 변수 사용;
}
참고 자료
김성우 저, "TCP/IP 윈도우 소켓 프로그래밍", 한빛아카데미, 2018