多線程之線程局部存儲
一 線程局部存儲 (TLS)
來自:http://msdn2.microsoft.com/en-us/library/ms686749.aspx
同一進程中的所有線程共享相同的虛擬地址空間。不同的線程中的局部變量有不同的副本,但是static和globl變量是同一進程中的所有線程共享的。使用TLS技術可以為static和globl的變量,根據當前進程的線程數量創建一個array,每個線程可以通過array的index來訪問對應的變量,這樣也就保證了static和global的變量為每一個線程都創建不同的副本。
二 線程局部存儲(TLS)實現使用
1)TLS 的 API 實現
通過 Win32 API 層和編譯器實現“線程本地存儲”。有關詳細信息,請參見 Win32 API 文檔中的 TlsAlloc、TlsGetValue、TlsSetValue 和 TlsFree。 (下面的代碼對msdn的稍加修改)
#include <windows.h>
#include <stdio.h>

#define THREADCOUNT 10
DWORD dwTlsIndex;

static int g_x = 100; // test static var for multiple threading

VOID ErrorExit(LPSTR);

VOID CommonFunc(VOID)


{
LPVOID lpvData;

// Retrieve a data pointer for the current thread.
lpvData = TlsGetValue(dwTlsIndex);
if ((lpvData == 0) && (GetLastError() != ERROR_SUCCESS))
ErrorExit("TlsGetValue error");
int *pg_x = (int*)lpvData;

// Use the data stored for the current thread.
printf("thread %d: g_x adress=%lx,g_x copy ++ = %d\n", GetCurrentThreadId(), pg_x, *pg_x);
Sleep(1000);
}

DWORD WINAPI ThreadFunc(VOID)


{
LPVOID lpvData;

// Initialize the TLS index for this thread.
lpvData = (LPVOID) LocalAlloc(LPTR, 256);
//*(int*)lpvData = g_x;
int *pg_x = (int*)lpvData;
*pg_x = g_x;
if (! TlsSetValue(dwTlsIndex, lpvData))
ErrorExit("TlsSetValue error");

printf("thread %d: g_x adress=%lx,g_x copy = %d\n", GetCurrentThreadId(), pg_x, *pg_x);

InterlockedExchangeAdd(reinterpret_cast<long*>(pg_x),1);
CommonFunc();

// Release the dynamic memory before the thread returns.
lpvData = TlsGetValue(dwTlsIndex);
if (lpvData != 0)
LocalFree((HLOCAL) lpvData);

return 0;
}

int main(VOID)


{
DWORD IDThread;
HANDLE hThread[THREADCOUNT];
int i;

printf("main thread: g_x is :%d\n",g_x);

// Allocate a TLS index.
if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
ErrorExit("TlsAlloc failed");

//test for multiple static or global var
int dwTlsIndex2 = TlsAlloc();

// Create multiple threads.
for (i = 0; i < THREADCOUNT; i++)

{
hThread[i] = CreateThread(NULL, // default security attributes
0, // use default stack size
(LPTHREAD_START_ROUTINE) ThreadFunc, // thread function
NULL, // no thread function argument
0, // use default creation flags
&IDThread); // returns thread identifier

// Check the return value for success.
if (hThread[i] == NULL)
ErrorExit("CreateThread error\n");
}

for (i = 0; i < THREADCOUNT; i++)
WaitForSingleObject(hThread[i], INFINITE);

TlsFree(dwTlsIndex);

printf("main thread: g_x is :%d\n",g_x);
return 0;
}

VOID ErrorExit (LPSTR lpszMessage)


{
fprintf(stderr, "%s\n", lpszMessage);
ExitProcess(0);
}
運行結果:(可以看出不同的線程中的g_x變量有不同的地址,即有不同的拷貝,主線程變量在開始和最后都不被改變。)
PS: (TLS的實現原理與API解釋)
1:在多線程的進程中,為每一個static或是global變量創建一個void*的數組,使變量不同線程都有一個拷貝,然后拷貝的指針放入Void*的數組中。
2:TlsAlloc() 得到對應于一個static或global變量所對應的void*的數組的索引,這個用來標示不同static或global變量所對應的void*的數組。
3:LocalAlloc()用來分配變量在此線程的拷貝的指針。
4:TlsSetValue()用來把剛分配的指針加到所對應的void*的數組中,加入后一般對會對此指針賦值供此線程使用。
5:TlsGetValue()用來從對應的void*的數組中找到此線程所對應的拷貝的指針。
6: TlsFree() 釋放整個void*的指針數組。
2)TLS 的編譯器實現
為了支持 TLS,已將新屬性 thread 添加到了 C 和 C++ 語言,并由 Visual C++ 編譯器支持。使用 __declspec 關鍵字聲明 thread 變量。例如,以下代碼聲明了一個整數線程局部變量,并用一個值對其進行初始化:
__declspec( thread ) int tls_i = 1;
下面的代碼使用了VC提供的__declspec關鍵字來實現TLS,如果不使用TLS,全局變量g_x則在線程中輸出的結果都是1,2,3,4.。。。但是如果使用TLS則在線程中輸出的結果都是1,編譯器幫我們保證了每個線程都有一個副本。 我們還可以看到主線程在開始和最后輸出的g_x都是0,這也更說明了在線程有不同的副本。
#include <windows.h>
#include <stdio.h>
#define THREADCOUNT 10

DWORD dwTlsIndex;

//static int g_x = 0;

#define Thread __declspec(thread)
Thread static int g_x = 0;

VOID ErrorExit(LPSTR);
DWORD WINAPI ThreadFunc(VOID)


{
InterlockedExchangeAdd(reinterpret_cast<long*>(&g_x),1); //g_x+=1;
printf("thread id: %d, g_x++ = %d, g_x adress = %d\n",GetCurrentThreadId(),g_x,&g_x);
return 1;
}
int main(VOID)


{
DWORD IDThread;
HANDLE hThread[THREADCOUNT];
int i;

printf("main thread: g_x = %d, g_x adress = %d\n",g_x,&g_x);
// Create multiple threads.
for (i = 0; i < THREADCOUNT; i++)

{
hThread[i] = CreateThread(NULL, // default security attributes
0, // use default stack size
(LPTHREAD_START_ROUTINE) ThreadFunc, // thread function
NULL, // no thread function argument
0, // use default creation flags
&IDThread); // returns thread identifier
// Check the return value for success.
if (hThread[i] == NULL)
ErrorExit("CreateThread error\n");
}
for (i = 0; i < THREADCOUNT; i++)
WaitForSingleObject(hThread[i], INFINITE);

printf("main thread: g_x = %d, g_x adress = %d\n",g_x,&g_x);
return 0;
}
VOID ErrorExit (LPSTR lpszMessage)


{
fprintf(stderr, "%s\n", lpszMessage);
ExitProcess(0);

運行結果:

三 使用VC關鍵字實現TLS需要注意:
聲明靜態綁定線程的本地對象和變量時必須遵守下列原則:
- thread 屬性只能應用于數據聲明和定義。它不能用于函數聲明或定義。例如,以下代碼將生成一個編譯器錯誤:
#define Thread __declspec( thread )
Thread void func(); // This will generate an error.
- 只能在具有 static 作用域的數據項上指定 thread 修飾符。包括全局數據對象(包括 static 和 extern)、本地靜態對象和 C++ 類的靜態數據成員。不可以用 thread 屬性聲明自動數據對象。以下代碼將生成編譯器錯誤:
#define Thread __declspec( thread )
void func1()
{
Thread int tls_i; // This will generate an error.
}
int func2( Thread int tls_i ) // This will generate an error.
{
return tls_i;
}
- 線程本地對象的聲明和定義必須全都指定 thread 屬性。例如,以下代碼將生成錯誤:
#define Thread __declspec( thread )
extern int tls_i; // This will generate an error, since the
int Thread tls_i; // declaration and definition differ.
- thread 屬性不能用作類型修飾符。例如,以下代碼將生成一個編譯器錯誤:
char __declspec( thread ) *ch; // Error
- C++ 類不能使用 thread 屬性。但是,可以使用 thread 屬性將 C++ 類對象實例化。例如,以下代碼將生成一個編譯器錯誤:
#define Thread __declspec( thread )
class Thread C // Error: classes cannot be declared Thread.
{
// Code
};
C CObject;
因為允許使用 thread 屬性的 C++ 對象的聲明,因此下面兩個示例在語義上是等效的:
#define Thread __declspec( thread )
Thread class B
{
// Code
} BObject; // OK--BObject is declared thread local.
class B
{
// Code
};
Thread B BObject; // OK--BObject is declared thread local.
- 不將線程本地對象的地址視為常數,并且涉及此類地址的任何表達式都不視為常數。在標準 C 中,這種作法的效果是禁止將線程本地變量的地址用作對象或指針的初始值設定項。例如,C 編譯器將以下代碼標記為錯誤:
#define Thread __declspec( thread )
Thread int tls_i;
int *p = &tls_i; //This will generate an error in C.
但是,此限制不適用于 C++。因為 C++ 允許動態初始化所有對象,因此可以用使用線程本地變量地址的表達式初始化對象。實現此操作的方式與實現線程本地對象結構的方式相同。例如,以上顯示的代碼在作為 C++ 源文件編譯時不會生成錯誤。請注意:只有在其中獲取地址的線程仍然存在的情況下,線程本地變量的地址才有效。
- 標準 C 允許使用涉及引用自身的表達式初始化對象或變量,但只適用于非靜態作用域的對象。雖然 C++ 通常允許使用涉及引用自身的表達式動態初始化對象,但是這種類型的初始化不允許用于線程本地對象。例如:
#define Thread __declspec( thread )
Thread int tls_i = tls_i; // Error in C and C++
int j = j; // OK in C++, error in C
Thread int tls_i = sizeof( tls_i ) // Legal in C and C++
請注意:包含正在初始化的對象的 sizeof
表達式不建立對自身的引用且在 C 和 C++ 中都是合法的。
C++ 不允許此類對線程數據的動態初始化,因為將來可能要對線程本地存儲功能進行增強。
- 如果 DLL 將任何非本地數據或對象聲明為 __declspec(線程),動態加載該 DLL 時會導致保護錯誤。使用 LoadLibrary 加載所有 DLL 后,每當代碼引用非本地 __declspec(線程)數據時,將導致系統故障。由于線程的全局變量空間是在運行時分配的,因此此空間的大小是以應用程序的需求和所有靜態鏈接的 DLL 的需求相加為基礎計算出來的。使用 LoadLibrary 時,無法擴展此空間以允許放置用 __declspec(線程)聲明的線程本地變量。如果 DLL 可能是用 LoadLibrary 加載的,請在 DLL 中使用 TLS API(如 TlsAlloc)來分配 TLS。
四 DLL使用TLS :http://msdn2.microsoft.com/en-us/library/ms686997.aspx