TLS 解决什么问题
全局变量是所有线程共享的,多线程访问要么加锁,要么用原子操作。但有一类数据天生不该共享:错误码、当前用户上下文、日志缓冲区。为它们加锁纯属浪费——线程之间根本不想共享。
TLS(Thread Local Storage,线程局部存储)就是答案:同一个变量名下,每个线程持有独立副本。写法像全局变量,行为是线程私有。
最经典的例子是错误码:线程 A 调用失败设了错误码,线程 B 同时失败——如果错误码是普通全局变量,A 还没读就被 B 覆盖了。所以 errno 和 GetLastError() 的值都存在 TLS 里,每个线程读到的都是自己那次失败。
分类:静态 TLS 与动态 TLS
Windows 提供两套机制:
|
静态 TLS(__declspec(thread)) |
动态 TLS(TlsAlloc 系列 API) |
| 声明时机 |
编译期,写死在代码里 |
运行时申请,用几个领几个 |
| 能存什么 |
完整对象(有类型、有大小) |
一个指针大小的格子,复杂数据自己 new 出来存指针 |
| 释放责任 |
线程退出时系统自动回收 |
槽位要 TlsFree,槽里 new 的对象也要自己 delete |
| 典型使用者 |
自己写的 exe |
DLL 作者(DLL 不知道宿主环境,动态 TLS 最保险) |
静态 TLS:一个关键字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <windows.h> #include <cstdio>
__declspec(thread) int g_counter = 0;
DWORD WINAPI ThreadProc(LPVOID param) { int id = (int)(INT_PTR)param; for (int i = 0; i < 3; ++i) { ++g_counter; printf("线程 %d: g_counter = %d (地址 %p)\n", id, g_counter, &g_counter); } return 0; }
int main() { HANDLE h[3]; for (int i = 0; i < 3; ++i) h[i] = CreateThread(NULL, 0, ThreadProc, (LPVOID)(INT_PTR)i, 0, NULL); WaitForMultipleObjects(3, h, TRUE, INFINITE); for (int i = 0; i < 3; ++i) CloseHandle(h[i]); }
|
运行结果(地址因运行而异,关键是三个线程的地址各不相同、计数值各自从 0 涨到 3):
1 2 3 4
| 线程 0: g_counter = 1 (地址 0000007B1A2C0000) 线程 1: g_counter = 1 (地址 0000007B1A2C1000) 线程 2: g_counter = 1 (地址 0000007B1A2C2000) ...
|
如果 g_counter 是普通全局变量,三个线程抢同一块内存,最终值是几全看调度运气;加上 __declspec(thread) 后每个线程都是 3,一个锁字都不用写。
C++11 之后更推荐标准写法 thread_local int g_counter = 0;,跨平台,语义相同。
动态 TLS:API 四件套
模型是”槽位(slot)”:系统给每个进程一张槽位表(至少 64 个,最多 1088 个),每个线程在同一张表的同一索引下有自己独立的格子。TlsAlloc 领索引,TlsSetValue/GetValue 操作当前线程那一格。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <windows.h> #include <cstdio>
DWORD g_tlsIndex = TLS_OUT_OF_INDEXES;
DWORD WINAPI ThreadProc(LPVOID param) { int id = (int)(INT_PTR)param;
int* p = new int(id * 100); TlsSetValue(g_tlsIndex, p);
int* q = (int*)TlsGetValue(g_tlsIndex); printf("线程 %d: 取到 %d\n", id, *q);
delete p; return 0; }
int main() { g_tlsIndex = TlsAlloc(); if (g_tlsIndex == TLS_OUT_OF_INDEXES) return 1;
HANDLE h[2] = { CreateThread(NULL, 0, ThreadProc, (LPVOID)0, 0, NULL), CreateThread(NULL, 0, ThreadProc, (LPVOID)1, 0, NULL) }; WaitForMultipleObjects(2, h, TRUE, INFINITE); CloseHandle(h[0]); CloseHandle(h[1]);
TlsFree(g_tlsIndex); }
|
注意分工:TlsAlloc/TlsFree 是进程级的(索引全局共享),TlsSetValue/TlsGetValue 是线程级的(各写各的格子)。
为什么 DLL 章必讲 TLS
TLS 是 DLL 作者的基础设施。DLL 被任意进程加载、被进程里任意多个线程同时调用,要保存”每个线程的上下文”(比如 Hook 库记录当前线程是否在钩子里、防重入),几乎只能靠 TLS。标准套路挂在 DllMain 的线程通知上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| __declspec(dllexport) DWORD g_tlsIndex = TLS_OUT_OF_INDEXES;
BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID) { switch (reason) { case DLL_PROCESS_ATTACH: g_tlsIndex = TlsAlloc(); break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: delete TlsGetValue(g_tlsIndex); break; case DLL_PROCESS_DETACH: TlsFree(g_tlsIndex); break; } return TRUE; }
|
一个要记住的坑
静态 TLS 变量如果带非平凡构造/析构(比如 __declspec(thread) SomeClass obj;),放在隐式加载的 DLL 里,在老系统(Vista 之前)上可能直接导致 DLL 加载失败。所以 DLL 里要稳妥就选动态 TLS,或者用 C++11 thread_local——编译器会为它生成特殊的延迟初始化代码,绕开这个坑。
小结
1 2 3 4
| TLS = 同一名称, 每线程一份 ├── 静态: __declspec(thread) / thread_local, 编译期声明, 存完整对象, 自动回收 └── 动态: TlsAlloc/SetValue/GetValue/Free, 运行期领槽位, 存指针, 自己释放 └── DLL 的标准姿势: DllMain 里 PROCESS_ATTACH 领槽, THREAD_DETACH 清数据
|
回到并发工具箱看,TLS 是”不共享”的极端,互斥锁是”共享但排队”,原子操作是”共享但一锤定音”——同一个问题的三种取舍,按数据该不该共享来选。