질럿: 윈도우즈 App에서 주 메인 쓰레드가 종료되면,,,,동작 중에 생성했던 쓰레드들도 종료되나요?
신영진: 그렇지 않나요?
질럿: 나도 그런걸로 알고 있는데... 리눅스에서는 안그런것 같아서요질럿: 앞에 앉은 사람이... 주메인 쓰레드가 종료해도... 다른 쓰레드들은 안 죽는데요
백지훈: 그러면 메인 쓰레드가 종료하면 자식 쓰레드가 남아서 일을 처리한다는 건가요?
시작은 이랬다. 질럿님의 당연한 듯한 질문. 돌이켜 보니 사실 내가 스레드에 대해서 알고 있는 사실은 하나도 없었다. ㅠㅠ 윈도우 개발자가 생각하는 스레드는 무엇일까? 몇 년간 윈도우 환경에서만 개발을 해 온 나에게는 다음과 같은 막연한 생각들이 있었다.
- 스레드는 프로세스에 포함되는 구성 요소다.
- 프로세스는 반드시 하나 이상의 스레드로 구성된다.
- 커널은 스레드 단위로 작업을 스케줄링한다.
1번 때문에 스레드는 프로세스 보다는 가볍다는 장점이 있다. 가상 주소 공간을 필요하지 않기 때문이다. 또한 변수들이 공유된다는 특징도 생긴다. 그래서 동기화에 신경을 써야 한다. 2번은 프로세스는 실행 단위가 아님을 나타낸다. 프로세스는 단지 스레드를 수행시키기 위한 컨테이너 정도로 생각할 수 있다. 코드는 전적으로 스레드에 의해서 수행된다. 3번 때문에 컨텍스트 스위칭 비용이 발생한다. 이 때문에 다중 스레드가 싱글 스레드보다 효율이 느린 경우가 많다. 항상 스레드의 적절한 개수를 신경써야 한다.
이런 막연한 지식들을 바탕으로 질럿님의 질문으로 돌아가보자. 주(primary) 스레드가 종료되면 나머지 스레드도 종료될까? 여기에는 큰 함정이 숨어 있다. 바로 주(primary) 스레드라는 말이다. 통상적으로 윈도우 개발자들은 WinMain이나 main을 수행시킨 최초의 스레드를 주(primary) 스레드라고 말한다. 첫번째 스레드, first thread라 하지 않고 왜 주(primary) 스레드라고 말할까? 그건 나도 모른다. 하지만 그게 함정이다. 주(primary)라는 말은 뭔가 이 녀석이 순서 외에도 특권을 가지고 있다는 생각을 하게 만들기 때문이다. 그래서 보통 주(primary) 스레드가 죽으면 다른 것도 죽을 것이라고 생각한다.
DWORD WINAPI Func1(PVOID)
{
while(1)
{
printf("1\n");
Sleep(1000);
}
}
DWORD WINAPI Func2(PVOID)
{
while(1)
{
printf("2\n");
Sleep(1000);
}
}
int main()
{
DWORD tid;
CreateThread(NULL, 0, Func1, NULL, 0, &tid);
CreateThread(NULL, 0, Func2, NULL, 0, &tid);
return 0;
}
위와 같은 테스트 코드를 테스트 해보자. 메인이 끝나니 다른 것도 끝나는 것처럼 보인다. 이제 우리의 막연한 생각을 뒷바침해줄 근거까지 생겼다. "이제 뭐 더 이상 토론할 필요도 없는 것이다."라고 생각한다면 정말 제대로 함정에 빠지는 것이다.
함정에 빠지지 않기 위해서는 역으로 질문을 던져야 한다. main이 리턴되면 왜 프로그램이 끝나는 것일까? 주(primary) 스레드가 종료되었기 때문에 끝나는 것일까? 의심을 품고 디버깅을 해보면 답이 보인다. 메인 리턴 후의 경로를 살펴 보면 최종적으로 CRT 코드에서 ExitProcess를 호출하는 것을 볼 수 있다. 그렇다. 주(primary) 스레드가 끝나서 프로그램이 끝난게 아니라, 주(primary) 스레드 끝에서 ExitProcess를 호출해서 프로그램이 끝난 것이다. 그렇다면 ExitProcess를 호출하지 않는다면?
ExitThread(0);
return 0;
}
위와 같이 코드를 고쳐 보았다. 저 코드는 진짜 질문의 의도대로 주(primary) 스레드만 종료 시킨다. 테스트 해보면 알겠지만 남은 두 스레드로 프로그램은 버젓이 돌아간다. 두 스레드가 모두 종료되면 프로그램도 종료된다.
프로세스에 포함된 스레드 간에는 주, 종관계가 없다. 다 평등한 관계다. 따라서 주(primary) 스레드란 말도 잘못된 것이다. 시작 스레드 내지는 첫번째 스레드 정도가 더 정확한 용어라 할 수 있다. 또한 프로세스는 기본적으로 모든 스레드가 종료되어야 끝이난다.