멀티 스레드 프로그래밍은 윈도우 프로그래밍 중에서도 매우 어려운 부분에 속한다. 그럼에도 점점 더 PC의 코어 수는 늘어가고 멀티 스레드 프로그래밍은 점점 더 당연한 일이 되어가고 있다. 그렇다면 왜 멀티 스레드 프로그래밍이 어려운 것일까? 아마도 우리의 생각 자체가 병렬적으로 동시 처리하는 것에 익숙하지 않은 점에 그 근본적인 원인이 있을 것 같다. 어쨌든 이러한 멀티 스레드 프로그래밍을 잘하기 위해서는 반드시 컨텍스트 개념에 익숙해져야 한다. 다음과 같은 표현에 숨겨진 의미를 통해 멀티스레드 프로그래밍에서 이야기되는 컨텍스트 개념에 대해 알아보도록 하자.
해당 콜백 함수는 임의의 스레드 컨텍스트에서 호출된다.
콜백을 지원하는 윈도우 함수 설명에 단골 손님으로 등장하는 문장이다. 이 문장을 보고 반사적으로 콜백 함수를 어떻게 작성해야 하는지, 이 문장이 개발자에게 주는 경고가 무엇인지가 떠오른다면 멀티 스레드 프로그래밍의 가장 핵심적인 기본은 이해했다고 할 수 있다.
우선 문장의 내포된 의미를 파악하기 위해 스레드 컨텍스트 내지는 프로그래밍에서 흔히 말하는 컨텍스트라는 말에 대해 살펴보자. 컨텍스트를 우리 말로 번역하면 문맥이라는 말이 된다. 문맥이란 무엇인가? 우리가 일상생활에서 접하게 되는 문맥이라는 말의 의미는 다음과 같은 간단한 대화 속에서 그 의미를 쉽게 파악할 수 있다.
영희: 너 밥 먹었니?
철수: 응. 먹었어.
일반적으로 구두 표현은 상황을 통해 유추할 수 있는 정보는 대부분 삭제된다. 우리는 철수의 대답에는 포함되지 않았지만 상황을 통해 ‘먹었어’라는 말을 철수가 밥을 먹었다라는 사실로 자연스럽게 확장시킬 수 있다. 이렇게 유추의 근거를 제공하는 말이 놓인 상황을 우리는 문맥이라고 부른다. 프로그래밍에서 말하는 것 또한 이와 별반 다르지 않다. 멀티 스레드 프로그래밍이나 일반적인 프로그래밍 환경에서 말하는 컨텍스트라는 말은 해당 코드가 바인딩되어 실행되는 환경을 일컫는다. 물론 이렇게 이야기를 하더라도 바인딩되어 실행되는 환경이라는 것이 무엇인지 감이 잡히지 않는다. 다음과 같은 간단한 코드를 통해 컨텍스트의 의미를 좀 더 구체적으로 살펴보자.
std::list<ULONG> GlobalList;
void Callback()
{
GlobalList.push_back(GetTickCount());
}
int main()
{
RegisterCallback(Callback);
for(;;)
{
if(!GlobalList.empty())
{
cout << GlobalList.front(); << endl;
GlobalList.pop_front();
}
}
return 0;
}
RegisterCallback 함수는 입력으로 들어간 콜백 함수가 랜덤 주기로 호출되도록 등록하는 함수다. 만약 RegisterCallback 함수 설명에 ‘입력으로 들어온 콜백 함수는 등록한 스레드 컨텍스트에서 호출된다’라는 말이 있다면 위 코드는 아무런 문제 없이 동작한다. 하지만 이와는 다르게 ‘입력으로 들어온 콜백 함수는 시스템 스레드 컨텍스트에서 호출된다’는 말이 있다면 위 코드는 잘못 동작한다. 이유는 Callback이 호출되는 스레드와 main이 실행되는 스레드가 다르기 때문이다. 서로 다른 스레드가 GlobalList를 동시에 접근함에도 동기화 처리가 되어 있지 않기 때문에 최악의 경우에는 프로그램 크래시가 발생한다. 이와 같이 Callback이라는 코드가 바인딩되어 실행되는 스레드를 두고 우리는 컨텍스트라는 말로 표현한다.
다시 처음에 언급했던 문장으로 돌아가 보자. 이제 컨텍스트의 개념은 충분히 이해했으니 ‘임의의’라는 말에 대해 살펴볼 필요가 있다. ‘임의의’라는 말은 정해지지 않았음을 나타내는 말이다. 그러니 ‘임의의 스레드 컨텍스트’라는 말은 달리 표현하면 ‘정해지지 않은 스레드 컨텍스트’가 된다. 그렇다면 도대체 정해지지 않은 스레드 컨텍스트라는 표현이 개발자에게 해주고 싶은 속내는 무엇이었을까? ‘정해지지 않았음’이 프로그래머에게 해주고 싶은 진짜 이야기는 이 말이다. ‘네 멋대로 특정 스레드 컨텍스트에서 호출된다고 생각하지 말라. 이 코드는 네가 생각지도 못한 스레드 컨텍스트에서도 호출될 수 있다.’
먼 길을 왔다. ‘해당 콜백 함수는 임의의 스레드 컨텍스트에서 호출된다’는 문장을 보는 순간 여러분이 베스트 개발자라면 다음과 같은 생각을 반드시 해야 한다. ‘아, 이 함수가 실행되는 스레드는 정해지지 않았구나. 콜백 함수가 전역 데이터에 접근하는 경우에는 반드시 동기화 처리를 해줘야 하겠구나.’ 반대로 ‘해당 콜백 함수는 콜백을 등록한 스레드와 같은 컨텍스트에서 호출된다’는 문장을 보게 되면 역으로 ‘동기화 처리를 생략해서 최적화를 할 수 있겠군.’이라는 생각이 머릿속에 떠올라야 한다.