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);
}
};