在多线程环境中,当每个线程需要独立维护某些数据时,就需要用到线程本地存储(TLS, Thread-Local Storage)。不同的操作系统 TLS 的实现方式也不相同,但都提供了相应的接口。QT 作为一个可跨平台的编程工具,封装了不同操作系统对 TLS 的操作,通过 QThreadStorage 类为每个线程提供独立数据存储。1. QThreadStorage 官方说明QThreadStorage 是一个为线程提供独立数据存储的模板类,通过hasLocalData()、localData()、setLocalData() 访问和存储线程数据。如果 QThreadStorage 存储指针类型的数据,数据必须通过关键字 new 在堆上创建,QThreadStorage 拥有该数据的管理权,并在线程退出(正常结束或终止)时删除该数据。注意:QThreadStorage 的析构函数不会删除每个线程的数据,只有线程退出或多次调用 setLocalData() 时才会删除线程数据。QThreadStorage 可用于存储主线程的数据。当 QApplication 销毁时(不管主线程是否结束),QThreadStorage 会删除其存储的主线程数据。2. window 中 TLS 的操作方法windows 平台中 TLS 索引通常由 TlsAlloc 函数在进程或 DLL 初始化期间分配。分配 TLS 索引后每个线程都有自己的索引槽。在分配 TLS 索引时,线程存储槽初始化为 NULL。进程的每个线程使用 TLS 索引来访问自己的 TLS 存储槽。线程存储数据时,指定 TLS 索引,调用 TlsSetValue 方法在其槽中存储值。线程检索存储数据时,指定相同的索引,调用用 TlsGetValue 检索存储的值。下图说明了 TLS 的工作原理。进程有两个线程:线程 1 和线程 2,分配两个 TLS 索引: gdwTlsIndex1 和 gdwTlsIndex2。 每个线程分配两个内存块(一个用于每个索引),用于存储数据,并将指向这些内存块的指针存储在相应的 TLS 槽中。 若要访问与索引关联的数据,线程将从 TLS 槽检索指向内存块的指针,并将其存储在 lpvData 本地变量中。使用线程本地存储的示例如下:#include windows.h #include stdio.h #define THREADCOUNT 4 // TLS 索引 DWORD dwTlsIndex; VOID ErrorExit (LPCSTR message); VOID CommonFunc(VOID) { LPVOID lpvData; // 获取线程本地存储的数据 lpvData = TlsGetValue(dwTlsIndex); if ((lpvData == 0) (GetLastError() != ERROR_SUCCESS)) ErrorExit("TlsGetValue error"); // 打印数据 printf("common: thread %d: lpvData=%lx\n", GetCurrentThreadId(), lpvData); Sleep(5000); } DWORD WINAPI ThreadFunc(VOID) { LPVOID lpvData; // 初始化线程本地数据 lpvData = (LPVOID) LocalAlloc(LPTR, 256); // 存储线程本地数据 if (! TlsSetValue(dwTlsIndex, lpvData)) ErrorExit("TlsSetValue error"); printf("thread %d: lpvData=%lx\n", GetCurrentThreadId(), lpvData); CommonFunc(); // 释放本地数据的存储空间 lpvData = TlsGetValue(dwTlsIndex); if (lpvData != 0) LocalFree((HLOCAL) lpvData); return 0; } int main(VOID) { DWORD IDThread; HANDLE hThread[THREADCOUNT]; int i; // 分配 TLS 索引 if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES) ErrorExit("TlsAlloc failed"); // 创建线程 for (i = 0; i THREADCOUNT; i++) { hThread[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) ThreadFunc, NULL, 0, IDThread); if (hThread[i] == NULL) ErrorExit("CreateThread error\n"); } for (i = 0; i THREADCOUNT; i++) WaitForSingleObject(hThread[i], INFINITE); // 释放 TLS 索引 TlsFree(dwTlsIndex); return 0; } VOID ErrorExit (LPCSTR message) { fprintf(stderr, "%s\n", message) ExitProcess(0); }3. linux 中 TLS 的操作方法Linux 中线程本地存储成为 TSD(Thread specific data),它是 POSIX 标准的一部分。每个线程都拥有一个私有内存块,即线程专用的数据区,简称 TSD 区。该区域由 TSD 键索引。TSD 键在所有线程中都是通用的,每个 TSD 键在每个线程中都指向自己的 TSD 区域。TSD 区域存储 void * 类型值。TSD 区域可以看作存储 void * 类型的数组,TSD 键作为整数索引,TSD 键对应数组元素的值的就是线程里存储的值。线程创建时,
从源码解析 QThreadStorage 的实现机制
在多线程环境中,当每个线程需要独立维护某些数据时,就需要用到线程本地存储(TLS, Thread-Local Storage)。不同的操作系统 TLS 的实现方式也不相同,但都提供了相应的接口。QT 作为一个可跨平台的编程工具,封装了不同操作系统对 TLS 的操作,通过 QThreadStorage 类为每个线程提供独立数据存储。1. QThreadStorage 官方说明QThreadStorage 是一个为线程提供独立数据存储的模板类,通过hasLocalData()、localData()、setLocalData() 访问和存储线程数据。如果 QThreadStorage 存储指针类型的数据,数据必须通过关键字 new 在堆上创建,QThreadStorage 拥有该数据的管理权,并在线程退出(正常结束或终止)时删除该数据。注意:QThreadStorage 的析构函数不会删除每个线程的数据,只有线程退出或多次调用 setLocalData() 时才会删除线程数据。QThreadStorage 可用于存储主线程的数据。当 QApplication 销毁时(不管主线程是否结束),QThreadStorage 会删除其存储的主线程数据。2. window 中 TLS 的操作方法windows 平台中 TLS 索引通常由 TlsAlloc 函数在进程或 DLL 初始化期间分配。分配 TLS 索引后每个线程都有自己的索引槽。在分配 TLS 索引时,线程存储槽初始化为 NULL。进程的每个线程使用 TLS 索引来访问自己的 TLS 存储槽。线程存储数据时,指定 TLS 索引,调用 TlsSetValue 方法在其槽中存储值。线程检索存储数据时,指定相同的索引,调用用 TlsGetValue 检索存储的值。下图说明了 TLS 的工作原理。进程有两个线程:线程 1 和线程 2,分配两个 TLS 索引: gdwTlsIndex1 和 gdwTlsIndex2。 每个线程分配两个内存块(一个用于每个索引),用于存储数据,并将指向这些内存块的指针存储在相应的 TLS 槽中。 若要访问与索引关联的数据,线程将从 TLS 槽检索指向内存块的指针,并将其存储在 lpvData 本地变量中。使用线程本地存储的示例如下:#include windows.h #include stdio.h #define THREADCOUNT 4 // TLS 索引 DWORD dwTlsIndex; VOID ErrorExit (LPCSTR message); VOID CommonFunc(VOID) { LPVOID lpvData; // 获取线程本地存储的数据 lpvData = TlsGetValue(dwTlsIndex); if ((lpvData == 0) (GetLastError() != ERROR_SUCCESS)) ErrorExit("TlsGetValue error"); // 打印数据 printf("common: thread %d: lpvData=%lx\n", GetCurrentThreadId(), lpvData); Sleep(5000); } DWORD WINAPI ThreadFunc(VOID) { LPVOID lpvData; // 初始化线程本地数据 lpvData = (LPVOID) LocalAlloc(LPTR, 256); // 存储线程本地数据 if (! TlsSetValue(dwTlsIndex, lpvData)) ErrorExit("TlsSetValue error"); printf("thread %d: lpvData=%lx\n", GetCurrentThreadId(), lpvData); CommonFunc(); // 释放本地数据的存储空间 lpvData = TlsGetValue(dwTlsIndex); if (lpvData != 0) LocalFree((HLOCAL) lpvData); return 0; } int main(VOID) { DWORD IDThread; HANDLE hThread[THREADCOUNT]; int i; // 分配 TLS 索引 if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES) ErrorExit("TlsAlloc failed"); // 创建线程 for (i = 0; i THREADCOUNT; i++) { hThread[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) ThreadFunc, NULL, 0, IDThread); if (hThread[i] == NULL) ErrorExit("CreateThread error\n"); } for (i = 0; i THREADCOUNT; i++) WaitForSingleObject(hThread[i], INFINITE); // 释放 TLS 索引 TlsFree(dwTlsIndex); return 0; } VOID ErrorExit (LPCSTR message) { fprintf(stderr, "%s\n", message) ExitProcess(0); }3. linux 中 TLS 的操作方法Linux 中线程本地存储成为 TSD(Thread specific data),它是 POSIX 标准的一部分。每个线程都拥有一个私有内存块,即线程专用的数据区,简称 TSD 区。该区域由 TSD 键索引。TSD 键在所有线程中都是通用的,每个 TSD 键在每个线程中都指向自己的 TSD 区域。TSD 区域存储 void * 类型值。TSD 区域可以看作存储 void * 类型的数组,TSD 键作为整数索引,TSD 键对应数组元素的值的就是线程里存储的值。线程创建时,