第 31 章 线程:线程安全与每线程存储 (Threads: Thread Safety and Per-Thread Storage)
核心结论
-
线程安全函数 = 多线程同时调用不会引入竞争;不可重入函数通常因使用全局/静态变量;SUSv3 要求除表 31-1 外所有函数线程安全;表 31-1 函数大多返回静态缓冲区指针(接口本身就是不可重入)。
-
使函数线程安全的 3 种方式:(1) per-function mutex(全函数序列化——简单但低并发);(2) critical section mutex(仅保护共享数据访问——更好并发);(3) reentrant 设计(无全局/静态——无锁但需改接口)。
-
pthread_once() 实现一次性初始化:用于「多个线程同时调第一个 init 时只 init 一次」;once_control 必须静态初始化 PTHREAD_ONCE_INIT;常用于动态 mutex 初始化 + 线程特定数据 key 创建。
-
线程特定数据 (TSD) 不改接口实现线程安全:pthread_key_create 创建 key(线程全局)+ destructor;pthread_setspecific/getspecific 存/取线程特定指针;首次调用时 malloc + setspecific 存指针;线程终止自动调 destructor 释放。
-
线程局部存储 (thread) 比 TSD 更简洁:GCC
thread关键字声明线程局部变量;编译期生效、零运行时开销;需 Linux 2.6 + NPTL + gcc 3.3+;每个线程一份变量,线程终止自动释放。 -
reentrant _r 函数族:SUSv3 定义 asctime_r/ctime_r/getgrgid_r/getlogin_r/getpwnam_r/gmtime_r/localtime_r/rand_r/strerror_r/strtok_r/ttyname_r 等;要求调用者传 buffer。
|
本章主旨
本章是 Pthreads 编程的「工程实践」章节——解决「已有不可重入库函数如何在多线程中安全使用」。读者应掌握:线程安全的判定标准(共享/静态变量 → 非线程安全)、3 种解决方案的取舍、pthread_once 用法、线程特定数据 API(key + setspecific/getspecific + destructor)的 4 步流程、线程局部存储 thread 关键字的简洁性、reentrant _r 函数族的存在意义。理解「为什么 strerror() 是经典案例」——它返回静态缓冲区指针,本质就是不可重入——是理解「线程安全 = 重入 + 同步」的关键。strerror_tsd 与 strerror_tls 两个实现对比,展示了从「手动 TSD」到「编译期 thread」的演进。 |
一、核心概念
本章围绕 6 个核心概念展开:从线程安全定义入手,到 3 种解决方案、pthread_once、线程特定数据 API(4 步流程)、线程局部存储,最后到 reentrant _r 函数族。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
线程安全 vs 可重入 |
线程安全:多线程同时调用不引入竞争;可重入:无全局/静态变量、可被中断/递归调用;SUSv3 要求除表 31-1 外函数线程安全;表 31-1 函数多因接口本身不可重入(返回静态 buffer)。 |
§31.1;表 31-1 含 asctime/basename/crypt/ctime/gethostbyname/getlogin/getenv/rand/readdir/strerror/strtok/ttyname 等;可重入版本加 _r 后缀。 |
3 种线程安全化方案 |
(1) per-function mutex:全函数序列化——简单但低并发;(2) critical section mutex:仅锁共享数据访问——更高并发;(3) reentrant:彻底去除全局/静态——无锁但需改接口。 |
§31.1;新库函数推荐 reentrant;改老函数 → TSD 或 TLS;高并发 critical section 优于 per-function mutex。 |
pthread_once() 一次性初始化 |
pthread_once_t once_control + void (*init)(void);保证 init 从任意线程调都只执行一次;once_control 必须静态初始化 PTHREAD_ONCE_INIT;典型用途——动态 mutex + TSD key 创建。 |
§31.2;早期 Pthreads 需 pthread_once + 静态 mutex;现代可用静态 mutex + static Boolean;pthread_once 主要用于 TSD key 创建。 |
线程特定数据 (TSD) 4 步流程 |
(1) pthread_key_create 创建全局 key + 注册 destructor;(2) pthread_once 保证 key 只创建一次;(3) 线程首次调用函数时 malloc 内存;(4) pthread_setspecific 存指针;后续 pthread_getspecific 取出。 |
§31.3;destructor 在线程终止时自动调,参数是 thread-specific pointer;典型实现——全局 key 数组 + 每线程指针数组;SUSv3 要求至少 128 keys(Linux 1024)。 |
线程局部存储 __thread |
GCC |
§31.4;比 TSD 简单得多——声明即用;同样适用于 strerror 重写;缺点——非标准(C11 有 _Thread_local 标准)。 |
reentrant _r 函数族 |
SUSv3 定义 asctime_r/ctime_r/getgrgid_r/getgrnam_r/getlogin_r/getpwnam_r/getpwuid_r/gmtime_r/localtime_r/rand_r/readdir_r/strerror_r/strtok_r/ttyname_r;调用者传 buffer 避免静态存储。 |
§31.1;glibc 还提供 crypt_r/gethostbyname_r/getservbyname_r 等非标 _r 版本;getaddrinfo 是 gethostbyname 的现代 reentrant 替代。 |
二、详细笔记
31.1 线程安全与可重入
What:线程安全函数可被多线程同时调用;可重入函数无全局/静态变量、可被中断/递归调用;SUSv3 列出「不要求线程安全」的函数清单(表 31-1)。
Why:理解「线程安全 = 重入 + 同步」是设计多线程库的基础;区分 SUSv3 强制的线程安全函数与不要求的函数是编写可移植多线程代码的前提。
How:
线程安全化方案对比(§31.1):
| 方案 | 优点 | 缺点 |
|---|---|---|
per-function mutex |
简单 |
序列化——并发性丧失 |
critical section mutex |
更高并发 |
需识别 critical section |
reentrant |
无锁——最快 |
必须改接口 |
TSD/TLS(不改接口) |
不改接口 |
略低于 reentrant 效率 |
SUSv3 非线程安全函数清单(§31.1,表 31-1 摘要):
-
密码学:
crypt(),encrypt()。 -
时间:
asctime(),ctime(),gmtime(),localtime(),mktime()。 -
字符串:
basename(),dirname(),strerror(),strtok()。 -
目录:
readdir()。 -
网络:
gethostbyname(),gethostbyaddr(),getnetbyname(),inet_ntoa()。 -
终端:
ttyname(),ptsname()。 -
环境:
getenv(),putenv(),setenv(),unsetenv()。 -
随机数:
rand(),drand48(),lrand48(),mrand48()。 -
登录:
getlogin(),cuserid()。 -
其他:
getopt(),nl_langinfo(),localeconv(),ftw(),nftw(),hcreate/hdestroy/hsearch()。
这些函数大多返回「静态分配 buffer 指针」或用 static 维护状态——接口本身就不可重入。
重入 vs 线程安全(§31.1):
-
可重入函数:无需 mutex 即可被多线程安全调用(无共享状态)。
-
线程安全函数:可能用 mutex 序列化调用,仍是线程安全但不一定可重入。
-
信号处理:可重入函数可安全用于信号 handler(详见第 21 章)。
SUSv4 修改(§31.1):
-
移除:ecvt/fcvt/gcvt/gethostbyname/gethostbyaddr(已从标准删除)。
-
新增:strsignal/system(system 因信号处置全局影响)。
When:写新库函数 → 设计为可重入;用老库函数 → 检查是否在表 31-1 中,若是则考虑 _r 版本或 TSD/TLS 重写。
Example(非线程安全 vs 线程安全):
// 摘自《The Linux Programming Interface》第 31 章 — Listing 31-1
/* 非线程安全:使用 static buffer */
static char buf[MAX_ERROR_LEN];
char *strerror(int err) {
if (err < 0 || err >= _sys_nerr) {
snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", err);
} else {
strncpy(buf, _sys_errlist[err], MAX_ERROR_LEN - 1);
}
return buf; /* 多线程同时调用 → 同一 buffer 竞争 */
}
31.2 pthread_once() 一次性初始化
What:pthread_once(&once_control, init) 保证 init 函数从任意线程调都只执行一次;once_control 静态初始化为 PTHREAD_ONCE_INIT。
Why:解决「库函数首次调用时初始化 mutex/分配 key」的同步问题;不用 pthread_once 可能多次初始化或竞争条件。
How:
API(§31.2):
// 摘自《The Linux Programming Interface》第 31 章
#include <pthread.h>
pthread_once_t once_var = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init)(void));
要点:
-
once_control 必须用 PTHREAD_ONCE_INIT 静态初始化。
-
init 函数签名
void init(void),无参数。 -
多个线程同时首次调 pthread_once → 一个线程执行 init,其他线程阻塞直到 init 完成。
-
init 完成后所有线程继续;后续调用直接返回。
-
主要用于动态 mutex 初始化 + TSD key 创建。
可替代方案(§31.2):
-
现代代码可用「静态 mutex + static Boolean 标志」替代。
-
但 pthread_once 仍是处理 TSD key 创建的标准方式。
When:库函数首次调用时需创建 mutex/TSD key/分配资源 → pthread_once;应用代码一般不直接用。
Example:
// 摘自《The Linux Programming Interface》第 31 章
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t key;
static void create_key(void) {
pthread_key_create(&key, destructor);
}
char *strerror(int err) {
pthread_once(&once, create_key); /* 保证只 create 一次 */
/* ... 使用 key ... */
}
31.3 线程特定数据 (TSD)
What:通过 pthread_key + 每线程指针实现「不改接口的线程安全」;key 全局唯一、值每线程独立;线程终止时自动调 destructor。
Why:用 per-function mutex 或 per-critical-section mutex 改变函数行为或加锁开销大;reentrant 需改接口(破坏调用方代码);TSD 不改接口、不加锁、提供每线程状态。
How:
TSD API(§31.3):
// 摘自《The Linux Programming Interface》第 31 章
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));
int pthread_key_delete(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);
4 步使用流程(§31.3.2):
-
创建 key:
pthread_once(&once, create_key)→create_key内pthread_key_create(&key, destructor)。 -
首次 malloc:线程首次调用时
pthread_getspecific(&key)返回 NULL →malloc分配 buffer。 -
保存指针:
pthread_setspecific(&key, buf)。 -
后续调用:
pthread_getspecific(&key)取出 buf;线程终止时 Pthreads 自动调 destructor(buf)。
实现原理(§31.3.3):
-
全局
pthread_keys[]数组——每项含 in-use 标志 + destructor 指针。 -
每线程
tsd[]数组——与pthread_keys[]索引对应,存线程特定指针。 -
pthread_key_create 返回的 key 是
pthread_keys[]索引。 -
线程创建时所有 tsd 初始化为 NULL。
-
线程终止时遍历 tsd[],非 NULL 项调对应 destructor。
限制(§31.3.5):
-
SUSv3 要求至少 128 keys(_POSIX_THREAD_KEYS_MAX)。
-
Linux 支持 1024 keys。
-
通常 1 个 key 够用——多值放结构体里。
strerror() TSD 重写(§31.3.4,Listing 31-3):
// 摘自《The Linux Programming Interface》第 31 章 — Listing 31-3
// 摘自 threads/strerror_tsd.c
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerrorKey;
static void destructor(void *buf) { free(buf); }
static void createKey(void) {
pthread_key_create(&strerrorKey, destructor);
}
char *strerror(int err) {
pthread_once(&once, createKey);
char *buf = pthread_getspecific(strerrorKey);
if (buf == NULL) {
buf = malloc(MAX_ERROR_LEN);
pthread_setspecific(strerrorKey, buf);
}
/* ... 使用 buf ... */
return buf;
}
destructor 顺序(§31.3.3):
-
线程有多个 TSD 块 → destructor 调用顺序 SUSv3 未规定。
-
destructor 应设计为独立操作,不依赖其他 destructor。
When:库函数需要 per-thread 状态但不改接口——典型如 strerror、getenv(POSIX 不要求线程安全);嵌入线程局部缓存的库。
Example:见上述 strerror 重写。
31.4 线程局部存储 (__thread)
What:GCC __thread 关键字声明线程局部变量;编译期生效;零运行时开销。
Why:比 TSD 简单得多——声明即用;无 pthread_once、无 setspecific/getspecific、无 destructor 注册;C11 标准化为 _Thread_local。
How:
用法(§31.4):
// 摘自《The Linux Programming Interface》第 31 章 — Listing 31-4
// 摘自 threads/strerror_tls.c
static __thread char buf[MAX_ERROR_LEN];
char *strerror(int err) {
/* 直接使用 buf——无需 TSD 4 步 */
if (err < 0 || err >= _sys_nerr) {
snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", err);
} else {
strncpy(buf, _sys_errlist[err], MAX_ERROR_LEN - 1);
}
return buf;
}
要点(§31.4):
-
__thread关键字紧跟static或extern关键字。 -
可带初始化器:
static __thread int counter = 0;。 -
可用
&取地址——得到当前线程的地址。 -
变量在线程终止时自动释放(无需 destructor)。
-
需要 Linux 2.6 内核 + NPTL + gcc 3.3+(x86-32)。
When:库函数需要 per-thread 状态且 GCC 编译——优先用 __thread 而非 TSD;C11 起可移植用 _Thread_local。
Example:
// 摘自《The Linux Programming Interface》第 31 章
static __thread int call_count = 0;
void my_func(void) {
call_count++; /* 每线程独立计数 */
/* ... */
}
31.5 reentrant _r 函数族
What:SUSv3 为表 31-1 不可重入函数定义带 _r 后缀的可重入版本——要求调用者传 buffer。
Why:避免使用 TSD/TLS 改写库函数;调用者控制存储——更显式、零开销。
How:
SUSv3 reentrant 函数(§31.1):
-
时间类:
asctime_r,ctime_r,gmtime_r,localtime_r。 -
字符串:
strerror_r,strtok_r。 -
目录:
readdir_r。 -
终端:
ttyname_r,ptsname_r(glibc 扩展)。 -
用户:
getlogin_r,getpwnam_r,getpwuid_r,getgrnam_r,getgrgid_r。 -
网络:
gethostbyname_r,getservbyname_r,gethostent_r(glibc 扩展)。 -
随机数:
rand_r。 -
密码:
crypt_r(glibc 扩展)。
strerror_r 用法:
// 摘自《The Linux Programming Interface》第 31 章
#include <string.h>
/* XSI 兼容版本:返回 int,buffer 由调用者提供 */
int strerror_r(int errnum, char *strerrbuf, size_t buflen);
/* GNU 扩展:返回 char *(与 strerror 兼容) */
char *strerror_r(int errnum, char *buf, size_t n);
要点:
-
调用者传 stack/local 缓冲区——避免静态存储。
-
strerror_r 返回值依赖实现——XSI vs GNU 版本不同(POSIX.1-2017 标准化 GNU 版本)。
-
现代替代:
getaddrinfo替代gethostbyname、localtime_r替代localtime。
When:新代码用 _r 版本或现代替代(getaddrinfo 等);旧代码移植时按需替换。
Example:
// 摘自《The Linux Programming Interface》第 31 章
char buf[256];
const char *msg = strerror_r(errno, buf, sizeof(buf));
if (msg != NULL) fprintf(stderr, "Error: %s\n", msg);
三、关键图表
|
SUSv3 非线程安全函数清单(表 31-1 节选)
reentrant 版本加 _r 后缀;现代替代用 getaddrinfo 等。 |
|
TSD vs TLS 对比
|
四、思维导图
mindmap
root((第 31 章 线程安全))
线程安全
多线程同时调用
不引入竞争
SUSv3 强制除表 31 1
表 31 1 静态 buffer 接口
3 种方案
per function mutex 序列化
critical section mutex 锁共享
reentrant 改接口无全局
TSD TLS 不改接口
pthread once
一次性初始化
once control 静态
多线程首次调同步
早期 mutex init 用
TSD API
pthread key create
pthread setspecific
pthread getspecific
destructor 自动调
线程终止释放
4 步流程
128 keys 上限
strerror 重写案例
TSD 实现
pthread keys 全局数组
每线程 tsd 数组
key 是索引
destructor 顺序未定义
TLS
GCC thread 关键字
编译期生效
零运行时开销
static extern 紧跟
Linux 2.6 NPTL gcc 3.3
strerror 重写案例
reentrant r 函数
asctime r ctime r
gmtime r localtime r
strerror r strtok r
getpwnam r getlogin r
调用者传 buffer
现代替代 getaddrinfo
strerror 案例
非线程安全 static buf
TSD 版本 pthread once key
TLS 版本 __thread buf
两线程测试 EINVAL EPERM
五、重点与易错点
-
SUSv3 强制要求除表 31-1 外所有函数线程安全——写新多线程代码时,标准库函数假定线程安全;但表 31-1 函数(如 strerror/getenv/rand)需自处理。
-
线程安全 ≠ 可重入——线程安全可能用 mutex;可重入无锁无全局/静态;信号处理必须用可重入函数(详见第 21 章)。
-
3 种线程安全化方案的取舍——per-function mutex 简单但低并发;critical section mutex 更好;reentrant 需改接口;TSD/TLS 不改接口。
-
pthread_once 保证 init 只执行一次——once_control 必须 PTHREAD_ONCE_INIT 静态初始化;现代代码可用静态 mutex + Boolean 替代,但 TSD key 创建仍依赖 pthread_once。
-
TSD key 是进程全局——pthread_key_create 创建的 key 在所有线程间共享;每线程有独立的值(通过 setspecific/getspecific 关联)。
-
TSD 必须 pthread_once 创建 key——pthread_key_create 本身可能 race(多个线程同时调时);用 pthread_once 保证只 create 一次。
-
TSD destructor 在线终止时自动调——参数是 pthread_setspecific 设置的 value;通常用于 free malloc 的 buffer。
-
多个 TSD 的 destructor 调用顺序 SUSv3 未规定——destructor 应设计为独立操作,不依赖其他 destructor 的副作用。
-
TSD key 上限 SUSv3 ≥128,Linux 1024——通常 1 函数 1 key 够用;多值放结构体。
-
TLS (thread) 比 TSD 简单得多——声明即用,无 pthread_once/setspecific/getspecific/destructor;优先用 thread 除非需 POSIX-only。
-
thread 必须紧跟 static 或 extern——
static thread int x;而非__thread static int x;(gcc 早期版本允许但不规范)。 -
__thread 变量可用 & 取地址——但不同线程地址不同;可用此检查「同一变量的线程副本」。
-
C11 标准化 _Thread_local——
#include <threads.h>后可用_Thread_local替代__thread;更具可移植性。 -
reentrant _r 函数要求调用者传 buffer——避免静态存储;strerror_r/getpwnam_r/gmtime_r 等;现代替代——getaddrinfo 替代 gethostbyname。
-
strerror_r 返回值依赖实现——XSI 版返回 int;GNU 版返回 char*;POSIX.1-2017 标准化 GNU 版;写可移植代码需注意。
-
非线程安全函数常见症状:两线程同时调 → 一线程结果被另一线程覆盖(strerror 返回同一静态 buffer);解——用 _r 版本或 TSD/TLS 重写。
-
per-function mutex 牺牲并发——只有 critical section 需保护;过度使用 mutex 导致「伪并行」——实际上串行。
-
跨章衔接:第 21 章信号处理 → 可重入函数(信号 handler 安全);第 29 章线程基础 → 第 30 章同步 → 第 31 章线程安全与 TSD/TLS;第 33 章线程细节 → pthread_atfork 与 fork 时的 mutex/cond 处理;第 53 章 POSIX 信号量 → sem_t vs pthread_mutex_t 对比。
-