thread_local介绍
先看代码#include iostream thread_local int tv; static int sv; int iv; int main() { //code here from A,B,C,D }A. constexpr int *p sv;B. constexpr int *p tv;C. int * p tv;D. constexpr int *p iv;Answer:B逐项分析选项表达式变量类型地址是常量表达式?编译结果Aconstexpr int *p sv;static int静态存储期✓ 是✅ 合法Bconstexpr int *p tv;thread_local int线程存储期✗ 否❌ 非法Cint * p tv;普通指针赋值不要求常量✅ 合法Dconstexpr int *p iv;全局int静态存储期✓ 是✅ 合法关键依据C 标准 [expr.const]constexpr要求初始化器是 核心常量表达式core constant expression。对于取地址运算静态存储期对象static/全局地址在链接期就固定下来是常量表达式 → A、D 合法线程存储期对象thread_local每个线程一份独立副本地址只能在 线程启动并完成 TLS 初始化时 才确定 → 不是常量表达式 → B 非法线程局部存储只是定义了对象的生命周期而没有定义可访问性。thread_local控制的是 存储期storage duration不是访问范围线程 A 完全可以把自己 TLS 变量的地址传给线程 B语法允许但一旦线程 A 退出TLS 销毁线程 B 再访问就是 悬空指针——所以危险性过大的说法成立取地址在运行时计算 → 无法作为constexpr初始化器 ✓也就是说我们可以获取线程局部存储变量的地址并将其传递给其他线程并且其他线程可以在其生命周期内自由使用变量。不过这样做除了用于诊断功能以外没有实际意义而且其危险性过大一旦没有掌握好目标线程的明周期就很可能导致内存访问异常造成未定义的程序行为通常情况下是程序崩溃。使用取地址运算符取到的线程局部存储变量的地址是运行时被计算出来的它不是一个常量也就是说无法和constexpr结合取地址了。一个典型的例子就是errno:errno通常用于存储程序当中上一次发生的错误早期它是一个静态变量由于当时大多数程序是单线程的因此没有任何问题。但是到了多线程时代这种errno就不能满足需求了。设想一下一个多线程程序的线程A在某个时刻刚刚调用过一个函数正准备获取其错误码也正是这个时刻另外一个线程B在执行了某个函数后修改了这个错误码那么线程A接下来获取的错误码自然不会是它真正想要的那个。这种线程间的竞争关系破坏了errno的准确性导致不可确定的结果。为了规避由此产生的不确定性POSIX将errno重新定义为线程独立的变量为了实现这个定义就需要用到线程局部存储直到C11之前errno都是一个静态变量而从C11开始errno被修改为一个线程局部存储变量。标准errno的规定C89/C03仅要求是可修改的左值实现可自行决定POSIX早于 C11已经要求errno是 per-thread 的由errno.h宏展开为线程相关函数调用例如*__errno_location()C11正式规定多线程程序中errno是线程局部C11通过引用 C11 间接保证errno是线程局部所以更准确的说法是POSIX 早就要求errno线程化C11/C11 把这个要求写进了语言标准很多实现如 glibc也早在 C11 之前就把errno实现为 TLS 了。总结题目答案 B 正确 ——constexpr int *p tv;是 4 个选项中唯一不能通过编译的因为thread_local变量的地址是 运行时常量而非 核心常量表达式。核心规则constexpr指针的初始化器必须是常量表达式。取地址x是常量表达式 ⇔x具有 静态存储期static、namespace-scope 全局、extern变量 ✓thread_local、自动变量、堆对象 ✗thread_local的本质只规定 生命周期 线程生命周期不限制地址被传到其他线程使用但跨线程使用 TLS 地址非常危险生命周期未对齐 → UB。errno的演进方向正确从单线程时代的静态变量演进为线程局部变量POSIX → C11 → C11用以避免多线程下错误码被覆盖的竞态。