DLL 裡面使用TLS (Local Thread Storage) 的常見做法是:在DLLMain的DLL_PROCESS_ATTACH/DLL_THREAD_ATTACH 被呼叫的時候為每個執行緒(Thread)分配記憶體,而在DLL_THREAD_DETACH/DLL_PROCESS_DETACH 被呼叫的時候釋放記憶體。 MSDN文章《Using Thread Local Storage in a Dynamic-Link Library》 上有這樣的示例程式碼。
BOOL WINAPI DllMain(HINSTANCE hinstDLL, // DLL module handle
    DWORD fdwReason,                    // reason called
    LPVOID lpvReserved)                 // reserved
{
    LPVOID lpvData;
    BOOL fIgnore;
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            // Allocate a TLS index.
            if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)                return FALSE;
         case DLL_THREAD_ATTACH:
             lpvData = (LPVOID) LocalAlloc(LPTR, 256);  //為每個Thread分配記憶體
            if (lpvData != NULL)
                fIgnore = TlsSetValue(dwTlsIndex, lpvData);
            break;
         case DLL_THREAD_DETACH:
             lpvData = TlsGetValue(dwTlsIndex);
            if (lpvData != NULL)
                LocalFree((HLOCAL) lpvData);  //釋放記憶體
            break;
         case DLL_PROCESS_DETACH:
            lpvData = TlsGetValue(dwTlsIndex);
            if (lpvData != NULL)
                LocalFree((HLOCAL) lpvData);  //釋放記憶體
            TlsFree(dwTlsIndex);
            break;
         default:
            break;
    }
     return TRUE;
}
這段程式碼認為DLL_THREAD_DETACH 總是會被呼叫, 但實際情況並非如此。在某些情況下DLL_THREAD_DETACH並不會被呼叫, 結果造成記憶體洩漏。 接下來做2個簡單實驗說明這個問題。
實驗程式碼:
typedef void (__stdcall *FNSLEEP)();
void CallTestDLL()
{
    FNSLEEP pfnSleep = (FNSLEEP)::GetProcAddress(g_hDLLModule, “DoSleep”);
    ATLASSERT(pfnSleep);
    (*pfnSleep)();
}
DWORD WINAPI ThreadProc( LPVOID lpParam)
{
    CallTestDLL();
    return 0;
}  
g_hDLLModule = ::LoadLibrary(_T(“TestDLL.dll”));
ATLTRACE(“[Thread %d] LoadLibrary=0x%.8x
“, ::GetCurrentThreadId());
CallTestDLL();
const int MAX_THREAD = 2;
HANDLE hThread[MAX_THREAD];
for (int i=0; i < MAX_THREAD; i++)
{
   hThread[i] = ::CreateThread(NULL, 0, ThreadProc, 0, 0, NULL); 
}
Sleep(MAX_THREAD * 1000);
::FreeLibrary(g_hDLLModule);
輸出結果1:
[Thread 4976] DLL_PROCESS_ATTACH                //主執行緒
[Thread 4976] LoadLibrary=0x0ecbf9d4
[Thread 4976] DoSleep() in DLL
[Thread 7860] DLL_THREAD_ATTACH                  //CreateThread 產生的執行緒
[Thread 736] DLL_THREAD_ATTACH                    //CreateThread 產生的執行緒
[Thread 736] DoSleep() in DLL
[Thread 7860] DoSleep() in DLL
[Thread 736] DLL_THREAD_DETACH
[Thread 7860] DLL_THREAD_DETACH
[Thread 4976] DLL_PROCESS_DETACH                //主執行緒
以上輸入結果我們看到每個Thread 呼叫DLL函式DoSleep 立即結束,這時候DLL_THREAD_DETACH 被正常呼叫。 這時只要候稍微改一下程式碼,會看到完全不同的結果。
 DWORD WINAPI ThreadProc( LPVOID lpParam)
{
    CallTestDLL();
    DoSomethingElse();  // 延遲執行緒結束
    return 0;
}  
輸出結果2:
[Thread 7448] DLL_PROCESS_ATTACH              //主執行緒
[Thread 7448] LoadLibrary=0x0b1cf9d4
[Thread 7448] DoSleep() in DLL
[Thread 6872] DLL_THREAD_ATTACH
[Thread 6556] DLL_THREAD_ATTACH
[Thread 6556] DoSleep() in DLL
[Thread 6872] DoSleep() in DLL
[Thread 7448] DLL_PROCESS_DETACH             //主執行緒
我們發現,CreateThread 產生的執行緒並沒有呼叫DLL_THREAD_DETACH 。
結論:
如果是執行緒在DLL被解除安裝(呼叫FreeLibrary) 之前結束,則DLL_THREAD_DETACH 會被呼叫。 如果執行緒在DLL解除安裝之後結束,則DLL_THREAD_DETACH 不會被呼叫。