title: 线程与线程同步 tags: [
嵌入式Linux,
Linux应用编程,
多线程,
pthread,
线程同步,
互斥锁,
条件变量,
自旋锁,
读写锁,
生产者消费者,
IMX6ULL,
] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[02-进程线程与IPC/01-信号机制]]、[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/03-进程间通信]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/9. pthread多线程基础]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/10. 互斥锁]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/11. 条件变量]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/12. 读写锁与自旋锁]];深入 → [[Linux+C+C++技术体系梳理/2. Linux系统编程/13. 线程池模式]]
进程让程序"能同时做多件事",线程则让同一个程序内部同时做多件事。进程切换开销大、进程间通信麻烦,而同一进程内的线程共享地址空间,切换便宜、通信容易,因此线程是现代并发程序的主力。但共享也带来代价:多个线程同时读写同一变量会引发竞态,必须用互斥锁、条件变量、自旋锁、读写锁等同步机制来保护。本篇把"线程基础"和"线程同步"合并在一起讲完,学完即可独立编写生产级多线程程序。
线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位;一个线程是进程中一条单一顺序的控制流(执行路线),一个进程可以创建多个线程,多个线程并发运行、各执行不同任务。
启动一个程序时,操作系统创建一个进程,同时一个线程立刻运行,这个线程叫主线程。main() 就是主线程的入口函数,main() 执行的任务就是主线程的任务。因此:
pthread_create() 创建,称为主线程的子线程。主线程的重要性体现在两方面:
线程的特点:
同时,每个线程又有私有的部分:各自的调用栈(线程栈)、寄存器环境(register context)、线程本地存储(thread-local storage)。
flowchart TB
accTitle: 进程与线程的资源关系
accDescr: 同一进程内多个线程共享代码段、数据段、堆和打开的文件,但各自拥有独立的线程栈和寄存器环境。
subgraph P["进程(共享资源)"]
CODE["代码段(共享,只读)"]
DATA["数据段 / 堆(共享)"]
FD["文件描述符表(共享)"]
SIG["信号处理方式(共享)"]
subgraph T1["线程 1"]
S1["私有线程栈"]
R1["私有寄存器环境"]
L1["私有 TLS"]
end
subgraph T2["线程 2"]
S2["私有线程栈"]
R2["私有寄存器环境"]
L2["私有 TLS"]
end
subgraph T3["线程 3"]
S3["私有线程栈"]
R3["私有寄存器环境"]
L3["私有 TLS"]
end
end
线程也有自己的生命周期。新创建的线程进入就绪态,获得 CPU 调度后进入运行态;时间片耗尽或被更高优先级线程抢占时回到就绪态;执行到需要等待的操作(加锁、条件变量等待、sleep、阻塞 I/O 等)时进入阻塞态,条件满足或被唤醒后重新回到就绪态;执行 pthread_exit()、从 start 函数 return、或被取消请求在取消点生效时进入终止态。
stateDiagram-v2
accTitle: 线程生命周期状态
accDescr: 线程从创建到终止的状态迁移,包含就绪、运行、阻塞等待与终止,取消请求需到达取消点才生效。
[*] --> 就绪: pthread_create
就绪 --> 运行: 获得CPU调度
运行 --> 就绪: 时间片耗尽或被抢占
运行 --> 阻塞: 加锁失败或wait或sleep或阻塞IO
阻塞 --> 就绪: 条件满足或被唤醒
运行 --> 终止: pthread_exit或return
运行 --> 终止: 取消请求到达取消点
终止 --> [*]
创建多个子进程本质上就是多个单线程进程,同样能并发处理多任务。两种模型各有优劣:
多进程编程的劣势:
多线程编程的优势:
多线程也有缺点:编程难度高,需要考虑线程安全、信号处理等问题,编写与调试比单线程困难。多进程模型通常用于大型应用程序(如网络服务器),中小型程序使用较少。
| 对比项 | 多进程 | 多线程 |
|---|---|---|
| 地址空间 | 各自独立、隔离 | 共享同一进程地址空间 |
| 切换开销 | 大 | 小 |
| 创建速度 | 慢 | 快 |
| 通信方式 | 需要 IPC(管道、共享内存等) | 直接读写全局变量即可 |
| 健壮性 | 一个进程崩溃不影响其它进程 | 一个线程崩溃可能拖垮整个进程 |
| 资源占用 | 每进程独立资源 | 共享进程资源,额外仅线程栈等 |
| 典型场景 | 网络服务器、大型系统 | 中小型并发程序、GUI、数据处理 |
生活化比喻:
单核处理器(如 I.MX6ULL 的单核 Cortex-A7)只有一个执行单元,只能以并发方式运行系统中的线程;多核处理器可并行执行多个线程,且每个执行单元内部仍以并发方式调度多个线程。由于处理器速度极快,交替轮询一次的时间在宏观上几乎可忽略,所以"看起来"所有线程在同时运行。
每个进程有进程 ID(pid_t,非负整数),每个线程也有其标识,称为线程 ID。进程 ID 在整个系统中唯一,而线程 ID 只在所属进程上下文中才有意义。线程 ID 使用 pthread_t 数据类型表示。
| 函数 | 原型 | 参数 | 返回值 | 说明 |
|---|---|---|---|---|
pthread_self |
pthread_t pthread_self(void); |
无 | 当前线程 ID | 总是成功 |
pthread_equal |
int pthread_equal(pthread_t t1, pthread_t t2); |
两个线程 ID | 相等返回非 0,否则返回 0 | 跨平台比较线程 ID 的正确方式 |
在 Linux 下 pthread_t 实际是 unsigned long int,但其它系统不一定,所以应把 pthread_t 当作不透明类型,用 pthread_equal() 比较,而不是直接用 ==(调试打印时可以临时按其实际类型打印)。
线程 ID 的用途:很多线程函数(pthread_cancel()、pthread_detach()、pthread_join() 等)用线程 ID 标识目标线程;也可用特定线程 ID 作为动态数据结构的标签,标识数据结构的创建者或属主线程。
#include <pthread.h>
#include <stdio.h>
int main(void)
{
pthread_t tid = pthread_self();
printf("当前线程 ID<%lu>\n", (unsigned long)tid);
return 0;
}
| 项 | 内容 |
|---|---|
| 原型 | int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); |
| 头文件 | <pthread.h> |
| 链接 | -lpthread(pthread 不在 gcc 默认链接库中) |
参数 thread |
pthread_t *,成功时新线程 ID 保存在此 |
参数 attr |
pthread_attr_t *,为 NULL 表示使用默认属性 |
参数 start_routine |
函数指针,新线程从此函数开始运行,签名 void *(*)(void *) |
参数 arg |
传给 start_routine 的参数;可为 NULL |
| 返回值 | 成功返回 0;失败返回错误号(不设置 errno) |
关键点:
start_routine 只能接收一个 void * 参数,需要传多个参数时应封装成结构体,把结构体地址作为 arg;arg 一般应指向全局变量或堆变量,保证在线程生命周期内该对象一直存在;若指向栈变量,线程访问时可能已失效;pthread_create() 失败时返回错误码而不像系统调用那样设置 errno。每个线程有自己的一份 errno 副本(为兼容使用 errno 的函数),但线程函数中直接返回错误码更清晰,可把错误范围限制在出错函数内;线程创建成功后立即加入系统调度队列,获得 CPU 后从 start_routine() 开始运行。调用后无法确定系统先调度主线程还是新线程,若程序对执行顺序有强制要求,必须使用同步技术。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n",
getpid(), (unsigned long)pthread_self());
return (void *)0;
}
int main(void)
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if (ret) {
fprintf(stderr, "Error: %s\n", strerror(ret));
exit(-1);
}
printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n",
getpid(), (unsigned long)pthread_self());
sleep(1); /* 主线程休眠,避免新线程还没运行进程就结束了 */
exit(0);
}
编译运行:
gcc -Wall -o testApp testApp.c -lpthread
./testApp
两个线程的进程 ID 相同(同属一个进程),线程 ID 不同;Linux 下线程 ID 数值很大,看起来像一个指针。
终止线程的方式:
return 语句并返回指定值,返回值就是线程的退出码;pthread_exit();pthread_cancel() 取消线程;exit()、_exit() 或 _Exit(),将导致整个进程终止。| 项 | 内容 |
|---|---|
| 原型 | void pthread_exit(void *retval); |
| 头文件 | <pthread.h> |
参数 retval |
线程返回值(退出码),可由 pthread_join() 获取 |
| 返回值 | 无 |
要点:
pthread_exit() 等价于在线程 start 函数中执行 return,区别是可在 start 函数所调用的任意函数中调用它来终止线程;retval 所指向的内容不应分配在线程栈中(线程终止后栈内容是否有效不确定),start 函数的返回值同理;如果主线程调用 pthread_exit(),主线程终止,但其它线程仍正常运行,直到所有线程都终止进程才终止。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
printf("新线程 start\n");
sleep(1);
printf("新线程 end\n");
pthread_exit(NULL);
}
int main(void)
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if (ret) {
fprintf(stderr, "Error: %s\n", strerror(ret));
exit(-1);
}
printf("主线程 end\n");
pthread_exit(NULL); /* 主线程退出,但进程不结束,新线程继续运行 */
exit(0);
}
类似父进程用 wait()/waitpid() 回收子进程,线程用 pthread_join() 阻塞等待线程终止并获取退出码、回收资源。
| 项 | 内容 |
|---|---|
| 原型 | int pthread_join(pthread_t thread, void **retval); |
| 头文件 | <pthread.h> |
参数 thread |
需要等待的线程 ID |
参数 retval |
非 NULL 时将目标线程退出状态复制到 *retval;若线程被取消,则 *retval 为 PTHREAD_CANCELED;不关心可传 NULL |
| 返回值 | 成功返回 0;失败返回错误码 |
要点:
pthread_join() 会阻塞等待指定线程终止;若该线程已终止则立即返回;pthread_join() 等待同一线程,结果不确定;pthread_join() 回收;若线程终止后无人 join,该线程成为僵尸线程(与僵尸进程类似),浪费系统资源,积累过多将无法创建新线程。进程终止后,僵尸线程会被父进程回收;与 waitpid() 的显著差别:
pthread_join() 等待另一个线程(进程则是父进程唯一可 wait());不能以非阻塞方式调用 pthread_join()(waitpid() 可 WNOHANG)。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
printf("新线程 start\n");
sleep(2);
printf("新线程 end\n");
pthread_exit((void *)10);
}
int main(void)
{
pthread_t tid;
void *tret;
int ret;
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid, &tret);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
printf("新线程终止, code=%ld\n", (long)tret); /* 输出 10 */
exit(0);
}
有时需要向线程发送请求,要求它立刻退出,称为取消线程。
| 项 | 内容 |
|---|---|
| 原型 | int pthread_cancel(pthread_t thread); |
| 头文件 | <pthread.h> |
| 参数 | 目标线程 ID |
| 返回值 | 成功返回 0;失败返回错误码 |
| 说明 | 发出请求后立即返回,不等待目标线程退出 |
默认情况下目标线程也会立刻退出,行为如同调用了参数为 PTHREAD_CANCELED(即 (void *)-1)的 pthread_exit()。但线程可设置自己不被取消或控制如何被取消,因此 pthread_cancel() 只是提出请求。
| 函数 | 原型 |
|---|---|
| 设置取消状态 | int pthread_setcancelstate(int state, int *oldstate); |
| 设置取消类型 | int pthread_setcanceltype(int type, int *oldtype); |
两者"设置新值 + 取回旧值"都是原子操作,oldstate/oldtype 可为 NULL。成功返回 0,失败返回非 0 错误码。
state 取值:
PTHREAD_CANCEL_ENABLE:线程可被取消(新建线程与主线程的默认值);PTHREAD_CANCEL_DISABLE:线程不可被取消,收到取消请求时将其挂起,直到状态变为 ENABLE。type 取值(仅在状态为 ENABLE 时有效):
PTHREAD_CANCEL_DEFERRED:取消请求被挂起,直到线程到达某个取消点才响应(默认值);PTHREAD_CANCEL_ASYNCHRONOUS:可能在任意时间点取消线程,应用场景很少。当线程调用 fork() 创建子进程时,子进程会继承调用线程的取消状态和类型;调用 exec 函数时,新程序主线程的取消状态和类型重置为默认值(ENABLE + DEFERRED)。
取消点是一系列函数,只有执行到这些函数时才会真正响应取消请求。系统认为未到取消点时代码正在执行关键工作,不应被停止。常见取消点函数包括:
| 类别 | 函数 |
|---|---|
| I/O | read()、write()、open()、close()、pread()、pwrite()、fcntl() |
| 睡眠 | sleep()、nanosleep()、clock_nanosleep()、pause()、sigsuspend() |
| 线程/信号 | pthread_join()、pthread_cond_wait()、pthread_cond_timedwait()、pthread_testcancel()、sigwait() |
| 等待/多路复用 | wait()、waitpid()、select()、poll()、pselect()、accept()、connect() |
| IPC | mq_send()、mq_receive()、sem_wait()、msgrcv()、msgsnd() |
| 同步/文件 | fsync()、fdatasync()、msync()、system()、tcdrain() |
除这些函数外,不得将任何其它函数视为取消点(调用它们不会招致取消)。可用 man 7 pthreads 查看完整列表。
如果一个循环体不含取消点(如空 for(;;)),线程将永远无法被取消。此时可用 void pthread_testcancel(void); 手动产生一个取消点:若线程已有挂起的取消请求,调用该函数后线程立即终止。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
printf("新线程--running\n");
for ( ; ; )
sleep(1); /* sleep 是取消点,可被取消 */
return (void *)0;
}
int main(void)
{
pthread_t tid;
void *tret;
int ret;
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
sleep(1);
ret = pthread_cancel(tid); /* 发送取消请求 */
if (ret) {
fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid, &tret);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
printf("新线程终止, code=%ld\n", (long)tret); /* 输出 -1,即 PTHREAD_CANCELED */
exit(0);
}
把 start 函数改成不含取消点的空循环,主线程将无法取消它;在循环中加 pthread_testcancel() 后又能被取消。
默认线程终止时可由其它线程 pthread_join() 获取状态并回收资源。若不关心返回值、希望线程终止时系统自动回收,可将其分离。
| 项 | 内容 |
|---|---|
| 原型 | int pthread_detach(pthread_t thread); |
| 头文件 | <pthread.h> |
| 参数 | 需要分离的线程 ID |
| 返回值 | 成功返回 0;失败返回错误码 |
要点:
pthread_detach(pthread_self());pthread_join() 获取其终止状态,且此过程不可逆;处于分离状态的线程终止后,系统自动回收其资源。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
int ret;
ret = pthread_detach(pthread_self()); /* 自行分离 */
if (ret) {
fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
return NULL;
}
printf("新线程 start\n");
sleep(2);
printf("新线程 end\n");
pthread_exit(NULL);
}
int main(void)
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
sleep(1);
ret = pthread_join(tid, NULL); /* 已分离,必然失败 */
if (ret)
fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); /* Invalid argument */
pthread_exit(NULL);
}
类似进程用 atexit() 注册终止处理函数,线程退出时也可执行线程清理函数。与进程不同,一个线程可以注册多个清理函数,记录在清理函数栈中(先进后出),执行顺序与注册顺序相反,全部执行完线程才终止。
| 项 | 内容 |
|---|---|
| 原型 | void pthread_cleanup_push(void (*routine)(void *), void *arg);void pthread_cleanup_pop(int execute); |
| 头文件 | <pthread.h> |
routine |
清理函数指针,无返回值,只有一个 void * 参数 |
arg |
调用清理函数 routine() 时传给它的参数 |
execute |
为 0 只移除栈顶清理函数不执行;非 0 则移除并执行该函数 |
清理函数栈中的函数在以下情况被执行:
pthread_exit() 退出时;pthread_cleanup_pop() 时。除此之外,其它终止方式不会执行清理函数,例如在线程 start 函数中执行 return 退出时不会执行。
注意:pthread_cleanup_push() 和 pthread_cleanup_pop() 实际是宏,会展开为 { 和 } 包裹的语句序列,因此必须在与线程相同的作用域中成对匹配使用,否则编译报错。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
static void cleanup(void *arg)
{
printf("cleanup: %s\n", (char *)arg);
}
static void *new_thread_start(void *arg)
{
printf("新线程--start run\n");
pthread_cleanup_push(cleanup, "第 1 次调用");
pthread_cleanup_push(cleanup, "第 2 次调用");
pthread_cleanup_push(cleanup, "第 3 次调用");
sleep(2);
pthread_exit((void *)0); /* 触发清理函数栈 */
/* 仅为与 pthread_cleanup_push 配对,否则编译不通过 */
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
}
int main(void)
{
pthread_t tid;
void *tret;
int ret;
ret = pthread_create(&tid, NULL, new_thread_start, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid, &tret);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
printf("新线程终止, code=%ld\n", (long)tret);
exit(0);
}
输出顺序为"第 3 次调用、第 2 次调用、第 1 次调用",验证了先进后出。若把 pthread_exit() 换成 return,则不会执行清理函数。
创建线程时可通过 pthread_attr_t 设置属性;attr 为 NULL 表示全部使用默认值。若自定义属性,需先用 pthread_attr_init() 初始化、用完用 pthread_attr_destroy() 销毁。
| 函数 | 原型 |
|---|---|
| 初始化/销毁属性 | int pthread_attr_init(pthread_attr_t *attr);int pthread_attr_destroy(pthread_attr_t *attr); |
| 栈地址与大小 | int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize); |
| 单独设置栈大小 | int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize); |
| 单独设置栈地址 | int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr); |
| 分离状态 | int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); |
detachstate 取值:
PTHREAD_CREATE_DETACHED:新建线程一开始便处于分离状态,结束后由系统回收资源,无法被 pthread_join();PTHREAD_CREATE_JOINABLE:默认值,正常启动,可被其它线程获取终止状态。设置栈大小为 4 KB 的示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
static void *new_thread_start(void *arg)
{
puts("Hello World!");
return (void *)0;
}
int main(int argc, char *argv[])
{
pthread_attr_t attr;
pthread_t tid;
int ret;
pthread_attr_init(&attr); /* 初始化属性对象 */
pthread_attr_setstacksize(&attr, 4096); /* 设置栈大小为 4K */
ret = pthread_create(&tid, &attr, new_thread_start, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
pthread_attr_destroy(&attr); /* 销毁属性对象 */
exit(0);
}
以分离状态启动线程:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, new_thread_start, NULL);
/* ... */
pthread_attr_destroy(&attr);
每个线程有自己独立的栈地址空间(线程栈),运行过程中的自动变量(局部变量)都分配在自己的线程栈中,互不干扰。创建线程时可配置栈大小与起始地址,多数情况保持默认即可。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static void *new_thread(void *arg)
{
int number = *((int *)arg);
unsigned long int tid = pthread_self();
printf("当前为<%d>号线程, 线程 ID<%lu>\n", number, tid);
return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{
pthread_t tid[5];
int j;
for (j = 0; j < 5; j++)
pthread_create(&tid[j], NULL, new_thread, &nums[j]);
for (j = 0; j < 5; j++)
pthread_join(tid[j], NULL);
exit(0);
}
5 个线程使用同一个 start 函数,但每个线程的栈中各有一份 number、tid,互不影响。
形成多条执行流有两种情况:一是多线程;二是信号处理——信号异步到来会打断主程序,从而在单线程进程内也形成主程序与信号处理函数两条执行流。
可重入函数:如果一个函数被同一进程的多个不同执行流同时调用,每次调用总能产生正确结果,则称为可重入函数。
可重入函数分两类:
反例(不可重入):
static int glob = 0;
static void func(int loops)
{
int local;
int j;
for (j = 0; j < loops; j++) {
local = glob; /* 读全局变量 */
local++;
glob = local; /* 写全局变量,多执行流并发将出错 */
}
}
很多 C 库函数都有可重入版本,名称后面加 _r,如 asctime()/asctime_r()、ctime()/ctime_r()、localtime()/localtime_r()。可用 man 查看函数的 ATTRIBUTES 信息。
线程安全函数:一个函数被多个线程(不包括信号处理函数产生的执行流)同时调用时,总能产生正确结果。
可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数——可重入函数是线程安全函数的真子集。区别在于:可重入只从语言语法角度分析(不涉及具体实现机制),而线程安全函数可以使用线程同步技术实现。例如把上面读写 glob 的函数加上互斥锁保护后,它变成线程安全函数,但因为修改了外部全局变量,仍不是可重入函数。
判断方法:用 man 手册查看 ATTRIBUTES,MT-Safe 表示线程安全、MT-Unsafe 表示线程不安全(MT = multithreaded)。POSIX.1-2001/2008 规定所有函数都必须是线程安全的,但有一批例外(asctime、ctime、gmtime、localtime、rand、strerror、strtok、getenv、readdir、system 等)。man 7 pthreads 可查看完整列表。
多线程环境下,有些初始化代码段只能执行一次。用 pthread_once() 保证 init_routine() 仅执行一次(由内核调度决定在哪个线程执行)。
| 项 | 内容 |
|---|---|
| 原型 | int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)); |
| 头文件 | <pthread.h> |
| 初始化 | pthread_once_t once_control = PTHREAD_ONCE_INIT; |
| 返回值 | 成功返回 0;失败返回错误编码 |
若一个线程调用 pthread_once() 时另一个线程也调用,后者会阻塞等待,直到第一个完成初始化返回。调用成功返回时,可确定所有状态都已初始化完成。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void initialize_once(void)
{
printf("initialize_once 被执行: 线程 ID<%lu>\n", pthread_self());
}
static void func(void)
{
pthread_once(&once, initialize_once); /* 只会执行一次 */
printf("函数 func 执行完毕.\n");
}
static void *thread_start(void *arg)
{
printf("线程%d 被创建: 线程 ID<%lu>\n",
*((int *)arg), pthread_self());
func();
pthread_exit(NULL);
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(void)
{
pthread_t tid[5];
int j;
for (j = 0; j < 5; j++)
pthread_create(&tid[j], NULL, thread_start, &nums[j]);
for (j = 0; j < 5; j++)
pthread_join(tid[j], NULL);
exit(0);
}
输出中 initialize_once() 只会被执行一次。
线程特有数据(线程私有数据)为每个调用线程维护一份变量副本,每个线程通过特有数据键(key) 访问时都拿到本线程绑定的副本,从而避免变量成为线程间共享数据。常用于把非线程安全函数改造为线程安全函数。
涉及 4 个函数:
| 函数 | 原型 | 说明 |
|---|---|---|
pthread_key_create |
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); |
创建 key,可绑定解构函数 |
pthread_setspecific |
int pthread_setspecific(pthread_key_t key, const void *value); |
保存线程私有缓冲区指针并与 key、当前线程关联 |
pthread_getspecific |
void *pthread_getspecific(pthread_key_t key); |
获取当前线程关联的私有缓冲区,未设置返回 NULL |
pthread_key_delete |
int pthread_key_delete(pthread_key_t key); |
删除 key(不触发解构函数,需先确保无线程使用) |
pthread_key_create() 只需在第一个调用线程中创建一次,因此通常配合 pthread_once() 使用。当使用该 key 的线程终止时,绑定的 destructor() 会被自动调用以释放私有数据。
典型用法(把返回静态缓冲区的 strerror 改造为线程安全版):
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#define MAX_ERROR_LEN 256
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerror_key;
static void destructor(void *buf)
{
free(buf); /* 释放线程私有缓冲区 */
}
static void create_key(void)
{
if (pthread_key_create(&strerror_key, destructor))
pthread_exit(NULL);
}
static char *my_strerror(int errnum)
{
char *buf;
if (pthread_once(&once, create_key)) /* 只创建一次 key */
pthread_exit(NULL);
buf = pthread_getspecific(strerror_key); /* 取本线程私有缓冲区 */
if (NULL == buf) { /* 首次调用,需分配 */
buf = malloc(MAX_ERROR_LEN);
if (NULL == buf)
pthread_exit(NULL);
if (pthread_setspecific(strerror_key, buf))
pthread_exit(NULL);
}
if (errnum < 0 || errnum >= _sys_nerr || NULL == _sys_errlist[errnum])
snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", errnum);
else {
strncpy(buf, _sys_errlist[errnum], MAX_ERROR_LEN - 1);
buf[MAX_ERROR_LEN - 1] = '\0';
}
return buf;
}
⚠️ 来源说明:
_GNU_SOURCE、_sys_errlist、_sys_nerr属于 GNU 扩展,用于演示;实际项目中更推荐使用可重入的strerror_r()。
线程局部存储比线程特有数据更简单:在全局或静态变量声明时加 __thread 修饰符,每个线程就都拥有一份该变量的拷贝,一直存在到线程终止时自动释放。
static __thread char buf[512];
注意事项:
static 或 extern,__thread 必须紧随其后;可用取值操作符 & 获取线程局部变量地址。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
static __thread char buf[100]; /* 每个线程一份拷贝 */
static void *thread_start(void *arg)
{
strcpy(buf, "Child Thread\n");
printf("子线程: buf (%p) = %s", buf, buf);
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
pthread_t tid;
int ret;
strcpy(buf, "Main Thread\n");
if (ret = pthread_create(&tid, NULL, thread_start, NULL)) {
fprintf(stderr, "pthread_create error: %d\n", ret);
exit(-1);
}
if (ret = pthread_join(tid, NULL)) {
fprintf(stderr, "pthread_join error: %d\n", ret);
exit(-1);
}
printf("主线程: buf (%p) = %s", buf, buf);
exit(0);
}
主线程和子线程打印出的 buf 地址不同,证明它们操作的是各自线程的副本。
Linux 信号模型基于进程设计,信号问世远早于线程,因此两者结合使用较为复杂。以下是信号在多线程下的行为映射规则:
SIGBUS、SIGFPE、SIGILL、SIGSEGV)由某个线程执行指令引起,系统将其发送给该线程;SIGPIPE 信号;pthread_kill() 或 pthread_sigqueue() 发出的信号。kill()/sigqueue()、终端 Ctrl+C 产生的 SIGINT 等)均属于进程层面;sigpending() 返回两者的并集。单线程程序用 sigprocmask() 设置进程信号掩码;多线程用 pthread_sigmask() 设置各线程的信号掩码,用法与 sigprocmask() 完全一样。
| 项 | 内容 |
|---|---|
| 原型 | int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset); |
| 头文件 | <signal.h> |
每个新建线程会从其创建者处继承信号掩码,之后可调用 pthread_sigmask() 改变自己的掩码。
| 函数 | 原型 | 说明 |
|---|---|---|
pthread_kill |
int pthread_kill(pthread_t thread, int sig); |
向同一进程中的指定线程发送信号;sig 为 0 时不发送信号但仍做错误检查 |
pthread_sigqueue |
int pthread_sigqueue(pthread_t thread, int sig, const union sigval value); |
与 sigqueue() 类似,但向指定线程发送并携带伴随数据 |
kill()/sigqueue() 针对整个进程;pthread_kill()/pthread_sigqueue() 针对某个线程。
异步信号安全函数:可以在信号处理函数中被安全调用的线程安全函数,要求比线程安全函数更严格。可重入函数一定是异步信号安全函数,线程安全函数则不一定是。
例如一个用互斥锁保护的线程安全函数,若信号处理函数中再次调用它,而处理信号的正是刚持有锁的线程,就会死锁。要将其实现为异步信号安全函数,可在获取锁之前通过设置信号掩码禁止接收该信号(将函数实现为不可被信号中断)。
常见异步信号安全函数(部分):_exit、open、close、read、write、fork、execve、kill、wait、waitpid、sigaction、sigprocmask、sigqueue、pipe、socket、connect、accept、pthread_kill、pthread_self、pthread_sigmask 等。可用 man 7 signal 查看完整列表。
一个安全的信号处理函数应满足:
注意:本书示例在信号处理函数中调用了非异步信号安全的
printf()仅为了打印方便,实际项目中不应这样用。
线程同步是为了对共享资源的访问进行保护,解决数据一致性问题。
本质原因是多个线程并发访问共享资源形成竞态(race),可能导致读写到无效值。例如线程 A 读变量、再写新值,写操作需两个时钟周期,线程 B 在中间读取,就会读到不一致的值。
用两个线程各递增全局变量 1000 万次来说明:期望结果 2000 万,实际结果往往小于它。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
l_count = g_count; /* 读全局变量到本地 */
l_count++; /* 本地递增 */
g_count = l_count; /* 写回全局变量 */
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
if (2 > argc)
loops = 10000000; /* 默认 1000 万次 */
else
loops = atoi(argv[1]);
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
printf("g_count = %d\n", g_count);
exit(0);
}
传参 1000 时结果正确(2000),传默认 1000 万时结果通常小于 2000 万,这就是数据不一致。
flowchart LR
accTitle: 并发访问共享变量导致数据不一致
accDescr: 两个线程交错读写同一全局变量,线程B在写操作未完成时读取,得到不一致的值。
subgraph A["线程 A"]
A1["读 g_count = 5"] --> A2["写 6(需 2 个时钟周期)"]
end
subgraph B["线程 B"]
B1["在写周期中间读 g_count"] --> B2["读到 5,而非 6"]
end
A2 -.->|"中间被读取"| B1
Linux 提供了多种线程同步机制:互斥锁、条件变量、自旋锁、读写锁等。
互斥锁(mutex,互斥量)本质是一把锁:访问共享资源前上锁,访问完成后解锁。上锁后任何其它试图再次加锁的线程都会被阻塞,直到当前线程释放锁。若有多个线程阻塞等待,解锁后它们会竞争,只有一个能成功上锁,其余继续阻塞。
程序设计的一个前提:所有访问共享资源的线程都必须遵守相同的加锁规则。若允许某个线程不拿锁就访问共享资源,即使其它线程都加锁,仍会出现数据不一致。
互斥锁使用 pthread_mutex_t 类型,两种初始化方式:
方式一:宏 PTHREAD_MUTEX_INITIALIZER(只适用于定义时直接初始化)。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
方式二:pthread_mutex_init()(适用于先定义后初始化、或堆中动态分配)。
| 项 | 内容 |
|---|---|
| 原型 | int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); |
| 头文件 | <pthread.h> |
参数 mutex |
待初始化的互斥锁 |
参数 attr |
互斥锁属性;NULL 表示默认属性(等价于宏方式,区别是宏方式不做错误检查) |
| 返回值 | 成功返回 0;失败返回非 0 错误码 |
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
/* 或堆上分配 */
pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);
| 函数 | 原型 | 说明 | 返回值 |
|---|---|---|---|
pthread_mutex_lock |
int pthread_mutex_lock(pthread_mutex_t *mutex); |
未锁定则上锁并返回;已被锁定则阻塞直到解锁 | 成功 0,失败非 0 |
pthread_mutex_trylock |
int pthread_mutex_trylock(pthread_mutex_t *mutex); |
未锁定则上锁返回;已锁定则不阻塞,返回 EBUSY |
成功 0,失败非 0(EBUSY) |
pthread_mutex_unlock |
int pthread_mutex_unlock(pthread_mutex_t *mutex); |
解锁 | 成功 0,失败非 0 |
以下行为均属错误:
多个线程阻塞等待时,解锁后哪个线程先上锁无法判断。
用互斥锁保护前面的 g_count 示例(默认 1000 万次也能得到正确结果):
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
pthread_mutex_lock(&mutex); /* 上锁 */
l_count = g_count;
l_count++;
g_count = l_count;
pthread_mutex_unlock(&mutex); /* 解锁 */
}
return (void *)0;
}
/* main 中创建线程前初始化:pthread_mutex_init(&mutex, NULL); */
以非阻塞方式加锁(效果相同,但会忙等):
while (pthread_mutex_trylock(&mutex)); /* 轮询直到拿到锁 */
/* 访问共享资源 */
pthread_mutex_unlock(&mutex);
互斥锁的加锁、临界区、解锁与唤醒竞争关系如下:
flowchart TD
accTitle: 互斥锁加锁解锁流程
accDescr: 线程访问共享资源前先加锁,锁空闲则进入临界区并在退出时解锁,锁被占用则阻塞等待,解锁后唤醒等待线程竞争。
A["访问共享资源前"] --> B["pthread_mutex_lock"]
B --> C{"锁是否空闲"}
C -->|是| D["加锁成功,进入临界区"]
C -->|否| E["阻塞休眠,等待锁释放"]
E --> C
D --> F["访问共享资源"]
F --> G["pthread_mutex_unlock"]
G --> H["唤醒其它等待线程竞争"]
| 项 | 内容 |
|---|---|
| 原型 | int pthread_mutex_destroy(pthread_mutex_t *mutex); |
| 返回值 | 成功返回 0;失败返回非 0 错误码 |
pthread_mutex_init() 后才能使用。更隐蔽的情况:一个线程需要同时访问两个由不同互斥锁保护的资源,多个线程以不同顺序加锁时可能死锁。
/* 线程 A */ /* 线程 B */
pthread_mutex_lock(mutex1); pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex2); pthread_mutex_lock(mutex1);
两个线程互相请求对方持有的锁,都无法向前运行。
避免方法:
mutex1 再锁 mutex2);pthread_mutex_lock() 锁定第一个锁,再用 pthread_mutex_trylock() 逐个尝试锁定其余锁;任一 trylock 失败(返回 EBUSY)就释放所有锁,过一段时间重试。这种方式效率较低,因为可能多次循环。pthread_mutexattr_t 定义互斥锁属性。使用非默认属性时,参数 attr 必须指向已初始化的属性对象。
| 函数 | 原型 |
|---|---|
| 初始化/销毁属性 | int pthread_mutexattr_init(pthread_mutexattr_t *attr);int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); |
| 获取/设置类型 | int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); |
4 种类型:
| 类型 | 行为 |
|---|---|
PTHREAD_MUTEX_NORMAL |
标准类型,不做错误检查或死锁检测;同一线程重复加锁会死锁;对未锁定或由其它线程锁定的锁解锁结果不确定 |
PTHREAD_MUTEX_ERRORCHECK |
提供错误检查:同一线程重复加锁、解锁他人锁定的锁、解锁未锁定的锁都会返回错误;运行较慢,可作调试工具 |
PTHREAD_MUTEX_RECURSIVE |
递归锁,允许同一线程在解锁前多次加锁并维护加锁次数;解锁次数必须等于加锁次数才真正释放 |
PTHREAD_MUTEX_DEFAULT |
默认行为;用 PTHREAD_MUTEX_INITIALIZER 或 attr 为 NULL 初始化的锁属于此类,Linux 下行为与 NORMAL 相仿 |
使用方式:
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
pthread_mutex_init(&mutex, &attr);
/* ... 使用 ... */
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
条件变量用于自动阻塞线程,直到某个特定事件发生或条件满足。它通常与互斥锁搭配使用,包含两个动作:
这里的"信号"指条件变量的通知,与第八章的 Linux 信号不是一回事。
条件变量不保存状态信息,只是传递应用程序状态信息的一种通讯机制。若在没有任何线程等待时发送信号,该信号会不了了之。
条件变量必须与互斥锁配合:因为条件检测通常需要访问共享资源,条件本身由互斥锁保护,线程改变条件状态前必须先锁住互斥锁。
| 函数 | 原型 |
|---|---|
| 初始化 | int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); |
| 销毁 | int pthread_cond_destroy(pthread_cond_t *cond); |
也可用宏 PTHREAD_COND_INITIALIZER 初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
attr 为 NULL 表示默认属性(等价于宏方式)。成功返回 0,失败返回非 0 错误码。
注意事项:
pthread_cond_init() 重新初始化。| 函数 | 原型 | 说明 |
|---|---|---|
pthread_cond_signal |
int pthread_cond_signal(pthread_cond_t *cond); |
至少唤醒一个等待线程,更高效 |
pthread_cond_broadcast |
int pthread_cond_broadcast(pthread_cond_t *cond); |
唤醒所有等待线程,总能得到正确结果 |
pthread_cond_wait |
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); |
阻塞等待,直到收到通知;成功 0,失败非 0 |
要点:
pthread_cond_signal() 和 pthread_cond_broadcast() 调用成功返回 0,失败返回非 0;pthread_cond_wait() 内部会对 mutex 操作:调用前线程必须已持有互斥锁;函数原子地把线程加入等待列表并解锁互斥锁;被唤醒返回时会再次锁住互斥锁;pthread_cond_broadcast() 唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。调用 pthread_cond_wait() 返回后,并不能确定判断条件是真还是假,必须立即重新检查;若条件不满足,继续休眠等待。因此必须用 while 循环:
while (0 >= g_avail)
pthread_cond_wait(&cond, &mutex);
原因:
条件变量有两个属性:进程共享属性和时钟属性,各有 get/set 方法,本书不深入介绍。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex; /* 定义互斥锁 */
static pthread_cond_t cond; /* 定义条件变量 */
static int g_avail = 0; /* 全局共享资源 */
/* 消费者线程 */
static void *consumer_thread(void *arg)
{
for ( ; ; ) {
pthread_mutex_lock(&mutex); /* 上锁 */
while (0 >= g_avail) /* 用 while 重新检查条件 */
pthread_cond_wait(&cond, &mutex);
while (0 < g_avail)
g_avail--; /* 消费 */
pthread_mutex_unlock(&mutex); /* 解锁 */
}
return (void *)0;
}
/* 主线程(生产者) */
int main(int argc, char *argv[])
{
pthread_t tid;
int ret;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
ret = pthread_create(&tid, NULL, consumer_thread, NULL);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
for ( ; ; ) {
pthread_mutex_lock(&mutex); /* 上锁 */
g_avail++; /* 生产 */
pthread_mutex_unlock(&mutex); /* 解锁 */
pthread_cond_signal(&cond); /* 通知消费者 */
}
exit(0);
}
消费者判断 g_avail <= 0 时调用 pthread_cond_wait() 阻塞并解锁;生产者 g_avail++ 后解锁并 pthread_cond_signal() 唤醒消费者,消费者被唤醒后自动重新加锁并消费。
sequenceDiagram
accTitle: 互斥锁与条件变量协作流程
accDescr: 消费者在条件不满足时等待,生产者生产后发信号唤醒,wait 原子解锁并在返回时重新加锁。
participant C as 消费者线程
participant M as 互斥锁
participant V as 条件变量
participant P as 生产者线程
C->>M: pthread_mutex_lock
C->>V: g_avail<=0 时 pthread_cond_wait
Note over C,M: wait 原子地解锁并阻塞
P->>M: pthread_mutex_lock
P->>P: g_avail++ 生产
P->>M: pthread_mutex_unlock
P->>V: pthread_cond_signal
V-->>C: 唤醒,wait 重新加锁返回
C->>C: 重新检查条件并消费
C->>M: pthread_mutex_unlock
pthread_cond_wait() 会无限期等待。若希望等待一段有上限的时间,可使用 pthread_cond_timedwait(),超时后返回错误码 ETIMEDOUT。
⚠️ 来源说明:
pthread_cond_timedwait()在《I.MX6U嵌入式Linux C应用编程指南》中仅作为"取消点函数"被列出,未展开讲解其原型与用法,以下为 POSIX 标准接口的扩展补充。
| 项 | 内容 |
|---|---|
| 原型 | int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); |
参数 abstime |
绝对时间(从 Epoch 起的秒 + 纳秒),不是相对时间 |
| 返回值 | 成功 0;超时返回 ETIMEDOUT;其它错误返回相应错误码 |
#include <time.h>
#include <errno.h>
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts); /* 取当前时间 */
ts.tv_sec += 3; /* 绝对超时时间:当前 +3 秒 */
pthread_mutex_lock(&mutex);
while (0 >= g_avail) {
if (ETIMEDOUT == pthread_cond_timedwait(&cond, &mutex, &ts)) {
/* 超时,做相应处理 */
break;
}
}
pthread_mutex_unlock(&mutex);
注意 abstime 是绝对时间,必须先取当前时间再加上超时时长。
自旋锁与互斥锁相似,本质也是一把锁。区别在于:
实现上,互斥锁基于自旋锁实现,所以自旋锁更底层。自旋锁的不足是:未获得锁时一直运行、占着 CPU,若不能很快获得锁会降低 CPU 效率。
试图对同一自旋锁加锁两次必然死锁;而同一互斥锁加锁两次不一定死锁(如 ERRORCHECK 类型会返回错误)。
自旋锁适用于需要保护的临界区执行时间很短的场景:持锁线程很快释放锁,自旋等待的线程只需等待很短时间,效率高。
| 对比项 | 互斥锁 | 自旋锁 |
|---|---|---|
| 实现层次 | 基于自旋锁实现,更上层 | 更底层 |
| 获取失败行为 | 阻塞休眠,唤醒后竞争 | 原地自旋,忙等 |
| 开销 | 休眠/唤醒开销大 | 忙等占用 CPU,但切换开销小 |
| 适用场景 | 临界区可能较长 | 临界区极短 |
| 中断上下文 | 不能用于中断服务函数 | 可用于中断服务函数,内核中会自动禁止抢占 |
| 函数 | 原型 |
|---|---|
| 初始化 | int pthread_spin_init(pthread_spinlock_t *lock, int pshared); |
| 销毁 | int pthread_spin_destroy(pthread_spinlock_t *lock); |
pshared 取值:
PTHREAD_PROCESS_SHARED:共享自旋锁,可在多个进程的线程间共享;PTHREAD_PROCESS_PRIVATE:私有自旋锁,只有本进程内的线程可用。成功返回 0,失败返回非 0 错误码。
| 函数 | 原型 | 说明 |
|---|---|---|
pthread_spin_lock |
int pthread_spin_lock(pthread_spinlock_t *lock); |
未锁定则上锁;已锁定则自旋等待 |
pthread_spin_trylock |
int pthread_spin_trylock(pthread_spinlock_t *lock); |
获取不到立即返回 EBUSY,不自旋 |
pthread_spin_unlock |
int pthread_spin_unlock(pthread_spinlock_t *lock); |
解锁 |
成功返回 0,失败返回非 0 错误码。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_spinlock_t spin; /* 定义自旋锁 */
static int g_count = 0;
static void *new_thread_start(void *arg)
{
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
pthread_spin_lock(&spin); /* 自旋锁上锁 */
l_count = g_count;
l_count++;
g_count = l_count;
pthread_spin_unlock(&spin); /* 自旋锁解锁 */
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
int ret;
if (2 > argc)
loops = 10000000;
else
loops = atoi(argv[1]);
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("g_count = %d\n", g_count);
pthread_spin_destroy(&spin);
exit(0);
}
对比可见,替换为自旋锁后程序运行耗时明显变短,但要注意使用场景。
互斥锁和自旋锁只有"加锁/不加锁"两种状态,一次只有一个线程能加锁。读写锁有 3 种状态:
一次只有一个线程可以占有写模式的读写锁,但可以有多个线程同时占有读模式的读写锁,因此读写锁比互斥锁并行性更高。读写锁也称共享互斥锁:读模式锁住称为共享模式锁住,写模式锁住称为互斥模式锁住。
两条规则:
因此读写锁非常适合读的次数远大于写的次数的场景。
| 函数 | 原型 |
|---|---|
| 初始化 | int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); |
| 销毁 | int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); |
也可用宏 PTHREAD_RWLOCK_INITIALIZER:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
成功返回 0,失败返回非 0 错误码。
| 函数 | 原型 | 说明 |
|---|---|---|
pthread_rwlock_rdlock |
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); |
以读模式加锁 |
pthread_rwlock_wrlock |
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); |
以写模式加锁 |
pthread_rwlock_unlock |
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); |
解锁(读/写模式通用) |
pthread_rwlock_tryrdlock |
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); |
非阻塞读加锁,失败返回 EBUSY |
pthread_rwlock_trywrlock |
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); |
非阻塞写加锁,失败返回 EBUSY |
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_rwlock_t rwlock; /* 定义读写锁 */
static int g_count = 0;
static void *read_thread(void *arg)
{
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++) {
pthread_rwlock_rdlock(&rwlock); /* 读模式获取锁 */
printf("读线程<%d>, g_count=%d\n", number + 1, g_count);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return (void *)0;
}
static void *write_thread(void *arg)
{
int number = *((int *)arg);
int j;
for (j = 0; j < 10; j++) {
pthread_rwlock_wrlock(&rwlock); /* 写模式获取锁 */
printf("写线程<%d>, g_count=%d\n", number + 1, g_count += 20);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{
pthread_t tid[10];
int j;
pthread_rwlock_init(&rwlock, NULL);
for (j = 0; j < 5; j++)
pthread_create(&tid[j], NULL, read_thread, &nums[j]);
for (j = 0; j < 5; j++)
pthread_create(&tid[j + 5], NULL, write_thread, &nums[j]);
for (j = 0; j < 10; j++)
pthread_join(tid[j], NULL);
pthread_rwlock_destroy(&rwlock);
exit(0);
}
读写锁只有一个属性——进程共享属性,与互斥锁、自旋锁相同。
| 函数 | 原型 |
|---|---|
| 初始化/销毁属性 | int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr); |
| 获取/设置共享属性 | int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared); |
pshared 取值:
PTHREAD_PROCESS_SHARED:共享读写锁,可在多个进程的线程间共享;PTHREAD_PROCESS_PRIVATE:私有读写锁,只有本进程内线程可用(默认值)。
pthread_rwlock_t rwlock;
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
pthread_rwlock_init(&rwlock, &attr);
/* ... 使用 ... */
pthread_rwlock_destroy(&rwlock);
pthread_rwlockattr_destroy(&attr);
| 机制 | 类型 | 状态数 | 获取失败行为 | 适用场景 |
|---|---|---|---|---|
| 互斥锁 | pthread_mutex_t |
2(锁/未锁) | 阻塞休眠 | 一般临界区保护,通用 |
| 条件变量 | pthread_cond_t |
无状态(配合互斥锁) | 阻塞等待通知 | 等待某个条件成立(生产者-消费者) |
| 自旋锁 | pthread_spinlock_t |
2(锁/未锁) | 原地自旋忙等 | 临界区极短;可用于中断上下文 |
| 读写锁 | pthread_rwlock_t |
3(读锁/写锁/未锁) | 读-读并发,读-写/写-写阻塞 | 读多写少 |
选择建议:实际开发中用得最多的是互斥锁和条件变量;临界区极短且不希望休眠时用自旋锁;读多写少时用读写锁。应根据场景选择,方能事半功倍。
下面把互斥锁与条件变量组合成一个完整的、可直接编译运行的生产者-消费者程序,使用一个固定容量的环形缓冲区,支持多个生产者与多个消费者。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define BUFFER_SIZE 8 /* 缓冲区容量 */
#define PRODUCER_NUM 2 /* 生产者数量 */
#define CONSUMER_NUM 3 /* 消费者数量 */
#define ITEM_TOTAL 20 /* 生产者各自生产的总数 */
static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; /* 保护缓冲区 */
static pthread_cond_t g_not_full = PTHREAD_COND_INITIALIZER; /* 缓冲区非满 */
static pthread_cond_t g_not_empty = PTHREAD_COND_INITIALIZER; /* 缓冲区非空 */
static int g_buffer[BUFFER_SIZE]; /* 环形缓冲区 */
static int g_in = 0; /* 生产写入位置 */
static int g_out = 0; /* 消费读出位置 */
static int g_count = 0; /* 当前元素个数 */
static int g_stop = 0; /* 生产结束标志 */
/* 向缓冲区放入元素 */
static void buffer_put(int value)
{
pthread_mutex_lock(&g_mutex);
while (g_count == BUFFER_SIZE) /* 缓冲区满则等待 */
pthread_cond_wait(&g_not_full, &g_mutex);
g_buffer[g_in] = value;
g_in = (g_in + 1) % BUFFER_SIZE;
g_count++;
printf("生产者: 放入 %d, 当前元素 %d\n", value, g_count);
pthread_cond_signal(&g_not_empty); /* 通知消费者非空 */
pthread_mutex_unlock(&g_mutex);
}
/* 从缓冲区取出元素,成功返回 0,无数据且已结束返回 -1 */
static int buffer_get(int *value)
{
int ret = 0;
pthread_mutex_lock(&g_mutex);
while (0 == g_count && !g_stop) /* 空且未结束则等待 */
pthread_cond_wait(&g_not_empty, &g_mutex);
if (0 == g_count && g_stop) { /* 已结束且无数据 */
ret = -1;
} else {
*value = g_buffer[g_out];
g_out = (g_out + 1) % BUFFER_SIZE;
g_count--;
printf("消费者: 取出 %d, 当前元素 %d\n", *value, g_count);
pthread_cond_signal(&g_not_full); /* 通知生产者非满 */
}
pthread_mutex_unlock(&g_mutex);
return ret;
}
/* 生产者线程 */
static void *producer_thread(void *arg)
{
int id = *((int *)arg);
int i;
for (i = 0; i < ITEM_TOTAL; i++) {
buffer_put(id * 100 + i);
usleep(1000); /* 模拟生产耗时 */
}
pthread_exit(NULL);
}
/* 消费者线程 */
static void *consumer_thread(void *arg)
{
int value;
(void)arg;
for ( ; ; ) {
if (0 != buffer_get(&value)) /* 结束且无数据则退出 */
break;
usleep(2000); /* 模拟消费耗时 */
}
pthread_exit(NULL);
}
int main(void)
{
pthread_t pid[PRODUCER_NUM];
pthread_t cid[CONSUMER_NUM];
int pnum[PRODUCER_NUM];
int i;
/* 创建生产者线程 */
for (i = 0; i < PRODUCER_NUM; i++) {
pnum[i] = i + 1;
if (pthread_create(&pid[i], NULL, producer_thread, &pnum[i])) {
fprintf(stderr, "create producer error\n");
exit(-1);
}
}
/* 创建消费者线程 */
for (i = 0; i < CONSUMER_NUM; i++) {
if (pthread_create(&cid[i], NULL, consumer_thread, NULL)) {
fprintf(stderr, "create consumer error\n");
exit(-1);
}
}
/* 等待生产者结束 */
for (i = 0; i < PRODUCER_NUM; i++)
pthread_join(pid[i], NULL);
/* 通知消费者生产已结束 */
pthread_mutex_lock(&g_mutex);
g_stop = 1;
pthread_cond_broadcast(&g_not_empty); /* 唤醒所有消费者 */
pthread_mutex_unlock(&g_mutex);
/* 等待消费者结束 */
for (i = 0; i < CONSUMER_NUM; i++)
pthread_join(cid[i], NULL);
pthread_mutex_destroy(&g_mutex);
pthread_cond_destroy(&g_not_full);
pthread_cond_destroy(&g_not_empty);
printf("全部线程结束\n");
return 0;
}
| 代码 | 说明 |
|---|---|
g_mutex / g_not_full / g_not_empty |
一个互斥锁保护缓冲区状态,两个条件变量分别表示"非满"和"非空" |
g_in / g_out / g_count |
环形缓冲区的写指针、读指针、元素个数,全部在互斥锁保护下修改 |
buffer_put() |
进入时加锁;用 while 检查缓冲区是否满,满则 pthread_cond_wait() 释放锁并休眠;放入后 signal 非空条件 |
buffer_get() |
用 while 检查空且未结束;g_stop 用于优雅退出——生产结束后消费者把剩余元素消费完再退出 |
pthread_cond_signal vs broadcast |
每次放入/取出用 signal 唤醒一个等待者;结束时用 broadcast 唤醒所有消费者 |
g_stop |
共享退出标志,同样在锁保护下读写,避免遗漏唤醒导致消费者永久阻塞 |
主线程 join 顺序 |
先等生产者全部结束,再设置 g_stop 并广播,最后等消费者退出,保证不丢数据 |
编译运行:
gcc -Wall -pthread -o pc pc.c
./pc
| 函数 | 一句话作用 |
|---|---|
pthread_self() |
获取当前线程 ID |
pthread_equal() |
比较两个线程 ID |
pthread_create() |
创建线程 |
pthread_exit() |
终止调用线程,可带退出码 |
pthread_join() |
阻塞等待线程终止并回收资源 |
pthread_detach() |
分离线程,终止后自动回收 |
pthread_cancel() |
向线程发送取消请求 |
pthread_testcancel() |
手动产生取消点 |
pthread_setcancelstate() |
设置可取消/不可取消 |
pthread_setcanceltype() |
设置延迟/异步取消 |
pthread_cleanup_push()/pop() |
注册/执行线程清理函数 |
| 函数 | 一句话作用 |
|---|---|
pthread_attr_init()/destroy() |
初始化/销毁线程属性对象 |
pthread_attr_setstacksize() |
设置线程栈大小 |
pthread_attr_setdetachstate() |
设置创建时是否分离 |
pthread_once() |
保证初始化代码只执行一次 |
pthread_key_create()/delete() |
创建/删除线程特有数据键 |
pthread_setspecific()/getspecific() |
设置/获取线程私有数据 |
| 对象 | 初始化 | 加锁/等待 | 解锁/通知 | 销毁 |
|---|---|---|---|---|
| 互斥锁 | pthread_mutex_init / PTHREAD_MUTEX_INITIALIZER |
pthread_mutex_lock / trylock |
pthread_mutex_unlock |
pthread_mutex_destroy |
| 条件变量 | pthread_cond_init / PTHREAD_COND_INITIALIZER |
pthread_cond_wait / timedwait |
pthread_cond_signal / broadcast |
pthread_cond_destroy |
| 自旋锁 | pthread_spin_init |
pthread_spin_lock / trylock |
pthread_spin_unlock |
pthread_spin_destroy |
| 读写锁 | pthread_rwlock_init / PTHREAD_RWLOCK_INITIALIZER |
pthread_rwlock_rdlock / wrlock |
pthread_rwlock_unlock |
pthread_rwlock_destroy |
在 Ubuntu 下建工作目录:
mkdir -p ~/vscode_ws/12_thread_sync
cd ~/vscode_ws/12_thread_sync
创建 race.c,粘贴 2.1 节的无锁版本,编译运行:
gcc -Wall -pthread -o race race.c
./race 10000000
观察 g_count 通常小于 2000 万。
创建 mutex.c,粘贴 2.2.2 的加锁版本,用同样的参数运行,观察结果稳定为 2000 万。
把互斥锁分别替换为自旋锁、读写锁,比较运行耗时(可用 time ./app)。
编译生产者-消费者程序并运行,观察"放入/取出"配对是否正常、程序能否正常退出。
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
链接报 undefined reference to 'pthread_create' |
未链接 pthread 库 | 编译加 -lpthread 或 -pthread |
g_count 结果偏小 |
共享变量未加锁,存在竞态 | 用互斥锁/自旋锁保护读写 |
| 程序卡死不退出 | 死锁;或条件变量等待无人唤醒 | 检查加锁顺序;确认有 signal/broadcast;用 while 重查条件 |
pthread_join 返回 Invalid argument |
目标线程已分离 | 分离的线程不能再 join |
| 取消请求不生效 | 线程处于不可取消状态,或循环内无取消点 | 检查 pthread_setcancelstate();加 pthread_testcancel() |
| 清理函数不执行 | 线程用 return 退出,或 pop 参数为 0 |
改用 pthread_exit(),或 pthread_cleanup_pop(1) |
pthread_cancel 返回非 0 |
pthread 函数失败返回错误码且不设置 errno | 用 strerror(ret) 翻译错误码 |
| 多线程程序崩溃 | 使用了线程不安全函数或共享数据未保护 | 查 man 的 ATTRIBUTES;确认 MT-Safe |
调试常用命令:
gcc -Wall -Wextra -pthread -g -o app app.c # 打开警告与调试信息
man 7 pthreads # 取消点、线程不安全函数列表
man 7 signal # 异步信号安全函数列表
gdb ./app # 调试
# (gdb) info threads / thread apply all bt # 查看所有线程与调用栈
死锁与数据竞争也可借助 helgrind、ThreadSanitizer(gcc -fsanitize=thread)等工具定位。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
| 维度 | I.MX6ULL(本教程) | STM32(裸机 / FreeRTOS) | RK3568 |
|---|---|---|---|
| 内核 | Cortex-A7(单核) | Cortex-M3/M4/M7 | Cortex-A55(多核) |
| 操作系统 | 完整 Linux | 裸机或 FreeRTOS 等 RTOS | 完整 Linux |
| 线程模型 | POSIX pthread(用户态线程) | 裸机无线程;RTOS 为任务(Task) | POSIX pthread,同 I.MX6ULL |
| 创建接口 | pthread_create() |
xTaskCreate()(FreeRTOS) |
pthread_create() |
| 同步机制 | mutex / cond / spin / rwlock(POSIX) | 信号量、互斥量、事件组(RTOS API) | 与 I.MX6ULL 相同 |
| 调度 | 内核 CFS 抢占式调度 | 优先级抢占式调度(RTOS) | 内核 CFS 调度 |
| 线程栈 | 每线程独立栈,可配置大小 | 每任务独立栈,创建时指定数组 | 每线程独立栈 |
| 内存保护 | MMU,用户/内核态隔离 | 无 MMU(M 核),无进程概念 | MMU,用户/内核态隔离 |
| 工具链 | arm-linux-gnueabihf-gcc -pthread |
arm-none-eabi-gcc + RTOS 源码 |
aarch64-linux-gnu-gcc -pthread |
| 迁移要点 | pthread 接口 Linux 间通用 | 需从 RTOS API 改写为 pthread | 与 I.MX6ULL 代码几乎一致,注意 64 位与库版本 |
结论:I.MX6ULL 与 RK3568 同属 Linux 用户态编程,本篇的 pthread API 可直接复用;STM32 裸机没有线程概念,使用 FreeRTOS 时则是另一套任务 API(xTaskCreate、xSemaphoreTake 等),概念相似但接口完全不同,切忌混用。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
答:
选择:需要频繁通信、频繁切换、要求高并发且共享数据多时用多线程(如数据处理、GUI);需要高隔离性、高健壮性、避免共享状态时用多进程(如网络服务器为每个请求 fork 子进程)。
答:pthread_create() 的 start_routine 签名固定为 void *(*)(void *),这是 POSIX 的规定,所以只能传递一个 void *。要传多个参数时,把这些参数封装到一个结构体中,把结构体地址作为 arg 传入。
关键约束:arg 指向的对象在线程整个生命周期内必须一直有效,因此应指向全局变量或堆变量,不能指向会失效的栈变量。例如:
struct thread_arg {
int id;
char name[32];
int result;
};
static void *worker(void *arg)
{
struct thread_arg *p = (struct thread_arg *)arg;
p->result = p->id * 2;
return NULL;
}
返回值也是 void *,可返回结构体指针(同样不应指向线程栈),由 pthread_join() 取出。
答:
pthread_join():阻塞等待目标线程终止并回收资源,可获取退出码。未分离线程必须 join,否则会变成僵尸线程。线程之间关系对等,任意线程都能 join 另一个线程;不能非阻塞调用。pthread_detach():把线程设为分离状态,线程终止时系统自动回收资源,之后不能再 join,且不可逆。适合不关心返回值的"即发即忘"线程。pthread_cancel():向线程发送取消请求,立即返回,不等待线程退出。默认行为如同 pthread_exit(PTHREAD_CANCELED)。线程可设置 PTHREAD_CANCEL_DISABLE 不被取消,取消类型分 DEFERRED(到取消点才响应)和 ASYNCHRONOUS(任意时刻)。区别总结:join 是"等待并回收",detach 是"让系统自动回收",cancel 是"请求对方退出"。
答:
| 机制 | 特点 | 适用场景 |
|---|---|---|
| 互斥锁 | 获取失败阻塞休眠,通用 | 一般临界区保护,使用最广 |
| 条件变量 | 无状态,配合互斥锁,等待条件成立 | 生产者-消费者、事件通知 |
| 自旋锁 | 获取失败原地自旋,不休眠,临界区必须极短 | 临界区极短;可用于中断上下文 |
| 读写锁 | 读-读可并发,读-写/写-写互斥 | 读远多于写的共享数据 |
补充:互斥锁基于自旋锁实现;自旋锁占用 CPU,长时间自旋会降低效率;读写锁提高了读场景的并行性,但实现更复杂、写者可能饥饿。实际工程中用得最多的是互斥锁 + 条件变量组合。
答:
man 查 ATTRIBUTES,MT-Safe 表示线程安全、MT-Unsafe 表示线程不安全;有 _r 后缀的通常可重入。一个安全的信号处理函数只能调用异步信号安全函数(如 write),不能调用 printf、malloc 等。内容来源:《I.MX6U嵌入式Linux C应用编程指南》第十一、十二章