Native C++로 작성한 소프트웨어를 C++/CLI로 포팅해야 하는 상황이었다. 컴파일러에 /clr 옵션을 주고 빌드하자 한번에 성공했다. 기뻐하며 F5 키를 눌러 실행시키는 순간, 런타임 오류가 발생했다.
각설하고 이틀 가량 조사하고 테스트한 결과, Thread Local Storage (TLS)의 문제였다. VC++에서는 __declspec 키워드를 사용하여 thread 변수를 선언할 수 있다.
__declspec( thread ) int tls_i = 1;
하지만 /clr 옵션이 들어가는 순간 __declspec 키워드를 쓸 생각 말아야 한다. Jeremy Kuhne는 CLR이 __declspec(thread)을 지원하지 않는다고 밝혔다. 그래서 그는 별도의 자원 관리 클래스를 만들어서, WIN32 API를 호출하게 했다. 컴파일러가 대신 해줄 일을 사람이 직접해야 하는 상황이 연출된 것이다. 물론 클래스만 제대로 설계하면 기존 코드를 손 댈 일이 거의 없긴 하다. Jeremy Kuhne가 소개한 클래스는 이렇게 생겨먹었다.
template <typename T> class ThreadLocal { private: DWORD threadLocalIndex; ThreadLocal(ThreadLocal const&); T *GetPointer(void) { return static_cast<T*>(::TlsGetValue(this->threadLocalIndex)); } void SetPointer(T *value) { ::TlsSetValue(this->threadLocalIndex, static_cast<void*>(value)); } public: void SetValue(const T &value) { T* currentPointer = this->GetPointer(); if (currentPointer == NULL) { this->SetPointer(new T(value)); } else { *currentPointer = value; } } T &GetValue(void) { T* currentPointer = this->GetPointer(); if (currentPointer == NULL) { this->SetPointer(new T()); } return *this->GetPointer(); } void DeleteValue() { T* currentPointer = this->GetPointer(); if (currentPointer != NULL) { delete currentPointer; this->SetPointer(NULL); } } ThreadLocal(const T& value) { this->threadLocalIndex = ::TlsAlloc(); this->SetValue(value); } ThreadLocal() { this->threadLocalIndex = ::TlsAlloc(); } virtual ~ThreadLocal() { this->DeleteValue(); ::TlsFree(this->threadLocalIndex); } };
__declspec(thread)가 지원 안 되는 정확한 이유는 모르겠다. 단지 테스트를 통해 이 사실을 확인하고, 그럴듯한 이유를 추정했을 뿐이다(확실하지 않은 내용이라 밝히진 않겠다). Jeremy Kuhne가 쓴 글 외에는 이 사실을 언급한 문서 자체가 없는 듯 하다. 혹시나 싶어 Visual Studio 2008 ‘Orcas’의 베타판으로 컴파일해봤지만, 결과는 마찬가지였다.
버그가 있었다. 초기화와 관련된 버그였는데, 간단하게 고쳐봤다. // modified 부분을 살펴보면 된다. 테스트하다 살짝 고친 코드라 또다른 버그가 있을 수도 있다.
#pragma unmanaged template <typename T> class ThreadLocal { private: DWORD threadLocalIndex; ThreadLocal(ThreadLocal const&); T *GetPointer(void) { return static_cast<T*>(::TlsGetValue(this->threadLocalIndex)); } void SetPointer(T *value) { ::TlsSetValue(this->threadLocalIndex, static_cast<void*>(value)); } T t; public: void SetValue(const T &value) { T* currentPointer = this->GetPointer(); if (currentPointer == NULL) { this->SetPointer(new T(value)); } else { *currentPointer = value; } } T &GetValue(void) { T* currentPointer = this->GetPointer(); if (currentPointer == NULL) { // modified this->SetPointer(new T(t)); } return *this->GetPointer(); } void DeleteValue() { T* currentPointer = this->GetPointer(); if (currentPointer != NULL) { delete currentPointer; this->SetPointer(NULL); } } ThreadLocal(const T& value) { this->threadLocalIndex = ::TlsAlloc(); // modified this->t = value; } ThreadLocal() { this->threadLocalIndex = ::TlsAlloc(); } virtual ~ThreadLocal() { this->DeleteValue(); ::TlsFree(this->threadLocalIndex); } };