--- title: 线程与线程同步 tags: [嵌入式Linux, Linux应用编程, 多线程, pthread, 线程同步, 互斥锁, 条件变量, 自旋锁, 读写锁, 生产者消费者, IMX6ULL] created: 2026-09-18 updated: 2026-09-18 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十一章 线程、第十二章 线程同步" --- # 线程与线程同步 > 💡 **关联知识**:[[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. 线程池模式]] 进程让程序"能同时做多件事",线程则让**同一个程序内部**同时做多件事。进程切换开销大、进程间通信麻烦,而同一进程内的线程共享地址空间,切换便宜、通信容易,因此线程是现代并发程序的主力。但共享也带来代价:多个线程同时读写同一变量会引发**竞态**,必须用互斥锁、条件变量、自旋锁、读写锁等同步机制来保护。本篇把"线程基础"和"线程同步"合并在一起讲完,学完即可独立编写生产级多线程程序。 --- ## 1. 线程基础 ### 1.1 什么是线程 **线程是参与系统调度的最小单位**。它被包含在进程之中,是进程中的实际运行单位;一个线程是进程中一条单一顺序的控制流(执行路线),一个进程可以创建多个线程,多个线程并发运行、各执行不同任务。 启动一个程序时,操作系统创建一个进程,同时一个线程立刻运行,这个线程叫**主线程**。`main()` 就是主线程的入口函数,`main()` 执行的任务就是主线程的任务。因此: - 任何一个进程都至少包含一个主线程,只有主线程的进程称为**单线程进程**; - 除主线程外还包含其它线程的,称为**多线程进程**,其它线程通常由主线程调用 `pthread_create()` 创建,称为主线程的**子线程**。 主线程的重要性体现在两方面: - 其它新线程(子线程)由主线程创建; - 主线程通常最后结束运行,负责各种清理工作,例如回收子线程。 线程的特点: - 线程不单独存在,而是包含在进程中; - 线程是参与系统调度的基本单位; - 可并发执行:同一进程的多个线程并发执行,宏观上表现为同时运行; - 共享进程资源:同一进程的各线程具有相同的地址空间,可访问进程已打开的文件、定时器、信号量等。 同时,每个线程又有**私有**的部分:各自的调用栈(线程栈)、寄存器环境(register context)、线程本地存储(thread-local storage)。 ```mermaid 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`、或被取消请求在取消点生效时进入**终止态**。 ```mermaid stateDiagram-v2 accTitle: 线程生命周期状态 accDescr: 线程从创建到终止的状态迁移,包含就绪、运行、阻塞等待与终止,取消请求需到达取消点才生效。 [*] --> 就绪: pthread_create 就绪 --> 运行: 获得CPU调度 运行 --> 就绪: 时间片耗尽或被抢占 运行 --> 阻塞: 加锁失败或wait或sleep或阻塞IO 阻塞 --> 就绪: 条件满足或被唤醒 运行 --> 终止: pthread_exit或return 运行 --> 终止: 取消请求到达取消点 终止 --> [*] ``` ### 1.2 线程 vs 进程 创建多个子进程本质上就是多个单线程进程,同样能并发处理多任务。两种模型各有优劣: **多进程编程的劣势:** - 进程间切换开销大。微观上仍是轮流切换,进程间切换开销远大于同一进程内多线程切换,对中小型程序不划算。 - 进程间通信麻烦。每个进程处于各自独立、隔离的地址空间,相互通信较麻烦。 **多线程编程的优势:** - 同一进程的多个线程间切换开销小; - 同一进程的多个线程间通信容易(共享地址空间); - 线程创建速度远大于进程创建速度; - 多线程在多核处理器上更有优势。 多线程也有缺点:编程难度高,需要考虑线程安全、信号处理等问题,编写与调试比单线程困难。多进程模型通常用于大型应用程序(如网络服务器),中小型程序使用较少。 | 对比项 | 多进程 | 多线程 | | ------ | ------ | ------ | | 地址空间 | 各自独立、隔离 | 共享同一进程地址空间 | | 切换开销 | 大 | 小 | | 创建速度 | 慢 | 快 | | 通信方式 | 需要 IPC(管道、共享内存等) | 直接读写全局变量即可 | | 健壮性 | 一个进程崩溃不影响其它进程 | 一个线程崩溃可能拖垮整个进程 | | 资源占用 | 每进程独立资源 | 共享进程资源,额外仅线程栈等 | | 典型场景 | 网络服务器、大型系统 | 中小型并发程序、GUI、数据处理 | ### 1.3 并发、并行与串行 - **串行**:一件事、一件事接着做,只有一个执行单元; - **并发**:交替做不同的事,同一个执行单元上把时间切成时间片,每个任务执行一段时间后切换,即**时分复用**; - **并行**:同时做不同的事,需要多个执行单元(多核)。 生活化比喻: - 吃饭吃到一半电话来了,吃完才去接——不支持并发也不支持并行,只是串行; - 停下吃饭去接电话,接完继续吃——支持并发; - 一边打电话一边吃饭——支持并行。 单核处理器(如 I.MX6ULL 的单核 Cortex-A7)只有一个执行单元,只能以**并发**方式运行系统中的线程;多核处理器可并行执行多个线程,且每个执行单元内部仍以并发方式调度多个线程。由于处理器速度极快,交替轮询一次的时间在宏观上几乎可忽略,所以"看起来"所有线程在同时运行。 ### 1.4 线程 ID 每个进程有进程 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 作为动态数据结构的标签,标识数据结构的创建者或属主线程。 ```c #include #include int main(void) { pthread_t tid = pthread_self(); printf("当前线程 ID<%lu>\n", (unsigned long)tid); return 0; } ``` ### 1.5 创建线程:pthread_create() | 项 | 内容 | | -- | ---- | | 原型 | `int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);` | | 头文件 | `` | | 链接 | `-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()` 开始运行。调用后无法确定系统先调度主线程还是新线程,若程序对执行顺序有强制要求,必须使用同步技术。 ```c #include #include #include #include #include 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); } ``` 编译运行: ```bash gcc -Wall -o testApp testApp.c -lpthread ./testApp ``` 两个线程的进程 ID 相同(同属一个进程),线程 ID 不同;Linux 下线程 ID 数值很大,看起来像一个指针。 ### 1.6 终止线程 终止线程的方式: - 线程的 start 函数执行 `return` 语句并返回指定值,返回值就是线程的退出码; - 线程调用 `pthread_exit()`; - 调用 `pthread_cancel()` 取消线程; - **注意**:如果进程中任意线程调用 `exit()`、`_exit()` 或 `_Exit()`,将导致**整个进程**终止。 | 项 | 内容 | | -- | ---- | | 原型 | `void pthread_exit(void *retval);` | | 头文件 | `` | | 参数 `retval` | 线程返回值(退出码),可由 `pthread_join()` 获取 | | 返回值 | 无 | 要点: - `pthread_exit()` 等价于在线程 start 函数中执行 `return`,区别是可在 start 函数所调用的**任意函数**中调用它来终止线程; - `retval` 所指向的内容**不应分配在线程栈中**(线程终止后栈内容是否有效不确定),start 函数的返回值同理; - 如果主线程调用 `pthread_exit()`,主线程终止,但其它线程仍正常运行,直到所有线程都终止进程才终止。 ```c #include #include #include #include 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); } ``` ### 1.7 回收线程:pthread_join() 类似父进程用 `wait()/waitpid()` 回收子进程,线程用 `pthread_join()` 阻塞等待线程终止并获取退出码、回收资源。 | 项 | 内容 | | -- | ---- | | 原型 | `int pthread_join(pthread_t thread, void **retval);` | | 头文件 | `` | | 参数 `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`)。 ```c #include #include #include #include #include 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); } ``` ### 1.8 取消线程:pthread_cancel() 有时需要向线程发送请求,要求它立刻退出,称为**取消线程**。 | 项 | 内容 | | -- | ---- | | 原型 | `int pthread_cancel(pthread_t thread);` | | 头文件 | `` | | 参数 | 目标线程 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);` 手动产生一个取消点:若线程已有挂起的取消请求,调用该函数后线程立即终止。 #### 取消示例 ```c #include #include #include #include #include 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()` 后又能被取消。 ### 1.9 分离线程:pthread_detach() 默认线程终止时可由其它线程 `pthread_join()` 获取状态并回收资源。若不关心返回值、希望线程终止时系统**自动回收**,可将其**分离**。 | 项 | 内容 | | -- | ---- | | 原型 | `int pthread_detach(pthread_t thread);` | | 头文件 | `` | | 参数 | 需要分离的线程 ID | | 返回值 | 成功返回 `0`;失败返回错误码 | 要点: - 线程既可以分离别的线程,也可以分离自己:`pthread_detach(pthread_self());` - 一旦处于分离状态,就**不能再使用 `pthread_join()`** 获取其终止状态,且此过程不可逆; - 处于分离状态的线程终止后,系统自动回收其资源。 ```c #include #include #include #include #include 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); } ``` ### 1.10 清理处理函数:pthread_cleanup_push/pop 类似进程用 `atexit()` 注册终止处理函数,线程退出时也可执行**线程清理函数**。与进程不同,一个线程可以注册多个清理函数,记录在**清理函数栈**中(先进后出),执行顺序与注册顺序相反,全部执行完线程才终止。 | 项 | 内容 | | -- | ---- | | 原型 | `void pthread_cleanup_push(void (*routine)(void *), void *arg);`
`void pthread_cleanup_pop(int execute);` | | 头文件 | `` | | `routine` | 清理函数指针,无返回值,只有一个 `void *` 参数 | | `arg` | 调用清理函数 `routine()` 时传给它的参数 | | `execute` | 为 `0` 只移除栈顶清理函数不执行;非 `0` 则移除并执行该函数 | 清理函数栈中的函数在以下情况被执行: - 线程调用 `pthread_exit()` 退出时; - 线程响应取消请求时; - 用**非 0**参数调用 `pthread_cleanup_pop()` 时。 除此之外,其它终止方式**不会**执行清理函数,例如在线程 start 函数中执行 `return` 退出时不会执行。 注意:`pthread_cleanup_push()` 和 `pthread_cleanup_pop()` 实际是**宏**,会展开为 `{` 和 `}` 包裹的语句序列,因此必须在与线程相同的作用域中**成对匹配**使用,否则编译报错。 ```c #include #include #include #include #include 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`,则不会执行清理函数。 ### 1.11 线程属性 创建线程时可通过 `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 的示例: ```c #include #include #include #include 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); } ``` 以分离状态启动线程: ```c 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); ``` ### 1.12 线程安全与可重入 #### 线程栈 每个线程有自己独立的栈地址空间(线程栈),运行过程中的自动变量(局部变量)都分配在自己的线程栈中,互不干扰。创建线程时可配置栈大小与起始地址,多数情况保持默认即可。 ```c #include #include #include 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`,互不影响。 #### 可重入函数(Reentrant) 形成多条执行流有两种情况:一是多线程;二是**信号处理**——信号异步到来会打断主程序,从而在单线程进程内也形成主程序与信号处理函数两条执行流。 **可重入函数**:如果一个函数被同一进程的多个不同执行流同时调用,每次调用总能产生正确结果,则称为可重入函数。 可重入函数分两类: - **绝对可重入函数**:无论怎么调用都可重入。特点是:函数内使用的变量均为局部变量(操作的内存地址均为本地栈地址);参数和返回值均为值类型;函数内调用的其它函数也都是绝对可重入函数。这类就是 pure code 可重入,多个副本使用分离的栈,互不干扰。 - **带条件的可重入函数**:满足某个/某些条件时才可重入。例如函数只读取全局变量而不修改它,可重入的前提是"多执行流调用期间该全局变量绝不被其它地方修改";参数为指针的函数,若每个执行流传入的都是自己本地变量的地址则安全,若传入共享变量地址则不安全。 反例(不可重入): ```c 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() 多线程环境下,有些初始化代码段只能执行一次。用 `pthread_once()` 保证 `init_routine()` 仅执行一次(由内核调度决定在哪个线程执行)。 | 项 | 内容 | | -- | ---- | | 原型 | `int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));` | | 头文件 | `` | | 初始化 | `pthread_once_t once_control = PTHREAD_ONCE_INIT;` | | 返回值 | 成功返回 `0`;失败返回错误编码 | 若一个线程调用 `pthread_once()` 时另一个线程也调用,后者会阻塞等待,直到第一个完成初始化返回。调用成功返回时,可确定所有状态都已初始化完成。 ```c #include #include #include 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()` 只会被执行一次。 #### 线程特有数据(TLS) 线程特有数据(线程私有数据)为每个调用线程维护一份变量副本,每个线程通过**特有数据键(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` 改造为线程安全版): ```c #define _GNU_SOURCE #include #include #include #include #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 线程局部存储比线程特有数据更简单:在全局或静态变量声明时加 `__thread` 修饰符,每个线程就都拥有一份该变量的拷贝,一直存在到线程终止时自动释放。 ```c static __thread char buf[512]; ``` 注意事项: - 变量声明中若用了 `static` 或 `extern`,`__thread` 必须紧随其后; - 与一般全局/静态变量一样,声明时可设初始值; - 可用取值操作符 `&` 获取线程局部变量地址。 ```c #include #include #include #include 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` 地址不同,证明它们操作的是各自线程的副本。 ### 1.13 线程与信号 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);` | | 头文件 | `` | 每个新建线程会从其创建者处继承信号掩码,之后可调用 `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()` 仅为了打印方便,实际项目中不应这样用。 --- ## 2. 线程同步 ### 2.1 为什么需要线程同步 线程同步是为了对**共享资源**的访问进行保护,解决**数据一致性**问题。 - 若每个线程访问的变量都是其它线程不会读写的(如各自局部变量、只有一个线程访问的全局变量),不存在一致性问题; - 变量只读时,多线程同时读取也没有问题; - 当一个线程可修改的变量,其它线程也能读取或修改时,就存在一致性问题,需要同步。 本质原因是多个线程**并发访问**共享资源形成**竞态(race)**,可能导致读写到无效值。例如线程 A 读变量、再写新值,写操作需两个时钟周期,线程 B 在中间读取,就会读到不一致的值。 用两个线程各递增全局变量 1000 万次来说明:期望结果 2000 万,实际结果往往小于它。 ```c #include #include #include #include #include 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 万,这就是数据不一致。 ```mermaid 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 提供了多种线程同步机制:互斥锁、条件变量、自旋锁、读写锁等。 ### 2.2 互斥锁(Mutex) 互斥锁(mutex,互斥量)本质是一把锁:访问共享资源前上锁,访问完成后解锁。上锁后任何其它试图再次加锁的线程都会被阻塞,直到当前线程释放锁。若有多个线程阻塞等待,解锁后它们会竞争,只有一个能成功上锁,其余继续阻塞。 程序设计的一个前提:**所有访问共享资源的线程都必须遵守相同的加锁规则**。若允许某个线程不拿锁就访问共享资源,即使其它线程都加锁,仍会出现数据不一致。 #### 2.2.1 初始化 互斥锁使用 `pthread_mutex_t` 类型,两种初始化方式: 方式一:宏 `PTHREAD_MUTEX_INITIALIZER`(只适用于定义时直接初始化)。 ```c pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; ``` 方式二:`pthread_mutex_init()`(适用于先定义后初始化、或堆中动态分配)。 | 项 | 内容 | | -- | ---- | | 原型 | `int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);` | | 头文件 | `` | | 参数 `mutex` | 待初始化的互斥锁 | | 参数 `attr` | 互斥锁属性;`NULL` 表示默认属性(等价于宏方式,区别是宏方式不做错误检查) | | 返回值 | 成功返回 `0`;失败返回非 0 错误码 | ```c pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); /* 或堆上分配 */ pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t)); pthread_mutex_init(mutex, NULL); ``` #### 2.2.2 加锁与解锁 | 函数 | 原型 | 说明 | 返回值 | | ---- | ---- | ---- | ------ | | `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 万次也能得到正确结果): ```c 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); */ ``` 以非阻塞方式加锁(效果相同,但会忙等): ```c while (pthread_mutex_trylock(&mutex)); /* 轮询直到拿到锁 */ /* 访问共享资源 */ pthread_mutex_unlock(&mutex); ``` 互斥锁的加锁、临界区、解锁与唤醒竞争关系如下: ```mermaid 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["唤醒其它等待线程竞争"] ``` #### 2.2.3 销毁 | 项 | 内容 | | -- | ---- | | 原型 | `int pthread_mutex_destroy(pthread_mutex_t *mutex);` | | 返回值 | 成功返回 `0`;失败返回非 0 错误码 | - 不能销毁还没有解锁的互斥锁; - 没有初始化的互斥锁也不能销毁; - 销毁后不能再上锁/解锁,需重新 `pthread_mutex_init()` 后才能使用。 #### 2.2.4 死锁 - 同一线程对同一互斥锁加锁两次,会陷入死锁; - 更隐蔽的情况:一个线程需要同时访问两个由不同互斥锁保护的资源,多个线程以不同顺序加锁时可能死锁。 ```c /* 线程 A */ /* 线程 B */ pthread_mutex_lock(mutex1); pthread_mutex_lock(mutex2); pthread_mutex_lock(mutex2); pthread_mutex_lock(mutex1); ``` 两个线程互相请求对方持有的锁,都无法向前运行。 避免方法: 1. **定义锁的层级关系**,所有线程总是按相同顺序对一组互斥锁加锁(如都先锁 `mutex1` 再锁 `mutex2`); 2. 当顺序难确定时,先 `pthread_mutex_lock()` 锁定第一个锁,再用 `pthread_mutex_trylock()` 逐个尝试锁定其余锁;任一 `trylock` 失败(返回 `EBUSY`)就释放所有锁,过一段时间重试。这种方式效率较低,因为可能多次循环。 #### 2.2.5 互斥锁属性与类型 `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 相仿 | 使用方式: ```c 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); ``` ### 2.3 条件变量(Condition Variable) 条件变量用于**自动阻塞线程**,直到某个特定事件发生或条件满足。它通常与互斥锁搭配使用,包含两个动作: - 一个线程等待某个条件满足而被阻塞; - 另一个线程在条件满足时发出"信号"。 > 这里的"信号"指条件变量的通知,与第八章的 Linux 信号不是一回事。 条件变量不保存状态信息,只是传递应用程序状态信息的一种通讯机制。若在没有任何线程等待时发送信号,该信号会不了了之。 条件变量必须与互斥锁配合:因为条件检测通常需要访问共享资源,条件本身由互斥锁保护,线程改变条件状态前必须先锁住互斥锁。 #### 2.3.1 初始化与销毁 | 函数 | 原型 | | ---- | ---- | | 初始化 | `int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);` | | 销毁 | `int pthread_cond_destroy(pthread_cond_t *cond);` | 也可用宏 `PTHREAD_COND_INITIALIZER` 初始化: ```c pthread_cond_t cond = PTHREAD_COND_INITIALIZER; ``` `attr` 为 `NULL` 表示默认属性(等价于宏方式)。成功返回 `0`,失败返回非 0 错误码。 注意事项: - 使用前必须初始化; - 对已初始化的条件变量再次初始化可能导致未定义行为; - 对未初始化的条件变量销毁可能导致未定义行为; - 仅当没有任何线程等待它时,销毁才是最安全的; - 销毁后可以再次 `pthread_cond_init()` 重新初始化。 #### 2.3.2 通知与等待 | 函数 | 原型 | 说明 | | ---- | ---- | ---- | | `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()` 唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。 #### 2.3.3 判断条件为什么用 while 而不是 if 调用 `pthread_cond_wait()` 返回后,并不能确定判断条件是真还是假,必须立即重新检查;若条件不满足,继续休眠等待。因此必须用 `while` 循环: ```c while (0 >= g_avail) pthread_cond_wait(&cond, &mutex); ``` 原因: - 多个线程等待同一条件时,任何线程都可能率先醒来获取互斥锁,并可能修改共享变量从而改变条件状态; - 可能存在**虚假通知**。 #### 2.3.4 条件变量的属性 条件变量有两个属性:进程共享属性和时钟属性,各有 get/set 方法,本书不深入介绍。 #### 2.3.5 条件变量示例 ```c #include #include #include #include #include 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()` 唤醒消费者,消费者被唤醒后自动重新加锁并消费。 ```mermaid 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 ``` #### 2.3.6 超时等待:pthread_cond_timedwait() `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`;其它错误返回相应错误码 | ```c #include #include 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` 是绝对时间,必须先取当前时间再加上超时时长。 ### 2.4 自旋锁(Spinlock) 自旋锁与互斥锁相似,本质也是一把锁。区别在于: - 互斥锁获取不到时线程**陷入阻塞(休眠)**,直到获取到锁被唤醒; - 自旋锁获取不到时在**原地自旋**(循环查看锁的持有者是否释放),一直占用 CPU。 实现上,互斥锁基于自旋锁实现,所以自旋锁更底层。自旋锁的不足是:未获得锁时一直运行、占着 CPU,若不能很快获得锁会降低 CPU 效率。 试图对同一自旋锁加锁两次**必然死锁**;而同一互斥锁加锁两次不一定死锁(如 ERRORCHECK 类型会返回错误)。 自旋锁适用于**需要保护的临界区执行时间很短**的场景:持锁线程很快释放锁,自旋等待的线程只需等待很短时间,效率高。 | 对比项 | 互斥锁 | 自旋锁 | | ------ | ------ | ------ | | 实现层次 | 基于自旋锁实现,更上层 | 更底层 | | 获取失败行为 | 阻塞休眠,唤醒后竞争 | 原地自旋,忙等 | | 开销 | 休眠/唤醒开销大 | 忙等占用 CPU,但切换开销小 | | 适用场景 | 临界区可能较长 | 临界区极短 | | 中断上下文 | 不能用于中断服务函数 | 可用于中断服务函数,内核中会自动禁止抢占 | #### 2.4.1 初始化与销毁 | 函数 | 原型 | | ---- | ---- | | 初始化 | `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 错误码。 #### 2.4.2 加锁与解锁 | 函数 | 原型 | 说明 | | ---- | ---- | ---- | | `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 错误码。 ```c #include #include #include #include #include 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); } ``` 对比可见,替换为自旋锁后程序运行耗时明显变短,但要注意使用场景。 ### 2.5 读写锁(Read-Write Lock) 互斥锁和自旋锁只有"加锁/不加锁"两种状态,一次只有一个线程能加锁。读写锁有 3 种状态: - 读模式下的加锁状态(读加锁); - 写模式下的加锁状态(写加锁); - 不加锁状态。 **一次只有一个线程可以占有写模式的读写锁,但可以有多个线程同时占有读模式的读写锁**,因此读写锁比互斥锁并行性更高。读写锁也称共享互斥锁:读模式锁住称为共享模式锁住,写模式锁住称为互斥模式锁住。 两条规则: - 读写锁处于**写加锁**状态时,解锁前所有试图加锁的线程(无论读模式还是写模式)都被阻塞; - 读写锁处于**读加锁**状态时,所有以读模式加锁的线程都能成功;任何以写模式加锁的线程都被阻塞,直到所有读模式锁被释放。 因此读写锁非常适合**读的次数远大于写的次数**的场景。 #### 2.5.1 初始化与销毁 | 函数 | 原型 | | ---- | ---- | | 初始化 | `int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);` | | 销毁 | `int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);` | 也可用宏 `PTHREAD_RWLOCK_INITIALIZER`: ```c pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; ``` 成功返回 `0`,失败返回非 0 错误码。 #### 2.5.2 上锁与解锁 | 函数 | 原型 | 说明 | | ---- | ---- | ---- | | `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` | ```c #include #include #include #include #include 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); } ``` #### 2.5.3 读写锁属性 读写锁只有一个属性——**进程共享属性**,与互斥锁、自旋锁相同。 | 函数 | 原型 | | ---- | ---- | | 初始化/销毁属性 | `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`:私有读写锁,只有本进程内线程可用(默认值)。 ```c 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); ``` ### 2.6 各同步机制对比 | 机制 | 类型 | 状态数 | 获取失败行为 | 适用场景 | | ---- | ---- | ------ | ------------ | -------- | | 互斥锁 | `pthread_mutex_t` | 2(锁/未锁) | 阻塞休眠 | 一般临界区保护,通用 | | 条件变量 | `pthread_cond_t` | 无状态(配合互斥锁) | 阻塞等待通知 | 等待某个条件成立(生产者-消费者) | | 自旋锁 | `pthread_spinlock_t` | 2(锁/未锁) | 原地自旋忙等 | 临界区极短;可用于中断上下文 | | 读写锁 | `pthread_rwlock_t` | 3(读锁/写锁/未锁) | 读-读并发,读-写/写-写阻塞 | 读多写少 | 选择建议:实际开发中**用得最多的是互斥锁和条件变量**;临界区极短且不希望休眠时用自旋锁;读多写少时用读写锁。应根据场景选择,方能事半功倍。 ### 2.7 生产者-消费者模型完整示例 下面把互斥锁与条件变量组合成一个完整的、可直接编译运行的生产者-消费者程序,使用一个固定容量的环形缓冲区,支持多个生产者与多个消费者。 ```c #include #include #include #include #include #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` 并广播,最后等消费者退出,保证不丢数据 | 编译运行: ```bash gcc -Wall -pthread -o pc pc.c ./pc ``` --- ## 3. API 速查表 ### 3.1 线程生命周期 | 函数 | 一句话作用 | | ---- | ---------- | | `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()` | 注册/执行线程清理函数 | ### 3.2 线程属性与数据 | 函数 | 一句话作用 | | ---- | ---------- | | `pthread_attr_init()/destroy()` | 初始化/销毁线程属性对象 | | `pthread_attr_setstacksize()` | 设置线程栈大小 | | `pthread_attr_setdetachstate()` | 设置创建时是否分离 | | `pthread_once()` | 保证初始化代码只执行一次 | | `pthread_key_create()/delete()` | 创建/删除线程特有数据键 | | `pthread_setspecific()/getspecific()` | 设置/获取线程私有数据 | ### 3.3 同步对象 | 对象 | 初始化 | 加锁/等待 | 解锁/通知 | 销毁 | | ---- | ------ | --------- | --------- | ---- | | 互斥锁 | `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` | --- ## 4. 实验步骤与调试方法 ### 4.1 实验步骤(以互斥锁修正竞态为例) 1. 在 Ubuntu 下建工作目录: ```bash mkdir -p ~/vscode_ws/12_thread_sync cd ~/vscode_ws/12_thread_sync ``` 2. 创建 `race.c`,粘贴 2.1 节的无锁版本,编译运行: ```bash gcc -Wall -pthread -o race race.c ./race 10000000 ``` 观察 `g_count` 通常小于 2000 万。 3. 创建 `mutex.c`,粘贴 2.2.2 的加锁版本,用同样的参数运行,观察结果稳定为 2000 万。 4. 把互斥锁分别替换为自旋锁、读写锁,比较运行耗时(可用 `time ./app`)。 5. 编译生产者-消费者程序并运行,观察"放入/取出"配对是否正常、程序能否正常退出。 ### 4.2 调试方法 | 现象 | 可能原因 | 排查手段 | | ---- | -------- | -------- | | 链接报 `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` | 调试常用命令: ```bash 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`)等工具定位。 --- ## 5. 跨平台对比:IMX6ULL vs STM32 vs RK3568 > ⚠️ **来源说明**:本节不属于《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` 等),概念相似但接口完全不同,切忌混用。 --- ## 6. 面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 ### Q1:线程和进程有什么区别?什么时候用多线程、什么时候用多进程? **答**: - **调度单位不同**:线程是参与系统调度的最小单位,进程只是资源分配的容器; - **地址空间不同**:同一进程内多个线程共享进程地址空间,进程之间相互独立、隔离; - **资源开销不同**:线程创建和切换开销远小于进程;线程共享进程的文件描述符、代码段、数据段、堆等,额外只有线程栈、寄存器、TLS 等; - **通信方式不同**:线程间可直接读写全局变量(需同步),进程间需要 IPC; - **健壮性不同**:一个线程崩溃可能拖垮整个进程;进程崩溃一般不影响其它进程; - **私有部分**:线程有各自的调用栈、寄存器环境、线程本地存储。 选择:需要频繁通信、频繁切换、要求高并发且共享数据多时用**多线程**(如数据处理、GUI);需要高隔离性、高健壮性、避免共享状态时用**多进程**(如网络服务器为每个请求 fork 子进程)。 ### Q2:pthread_create 的 start 函数为什么只能接收一个 void * 参数?如何传多个参数? **答**:`pthread_create()` 的 `start_routine` 签名固定为 `void *(*)(void *)`,这是 POSIX 的规定,所以只能传递一个 `void *`。要传多个参数时,把这些参数封装到一个**结构体**中,把结构体地址作为 `arg` 传入。 关键约束:`arg` 指向的对象在线程整个生命周期内必须一直有效,因此应指向**全局变量或堆变量**,不能指向会失效的栈变量。例如: ```c 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()` 取出。 ### Q3:pthread_join、pthread_detach、pthread_cancel 分别是什么?有什么区别? **答**: - **`pthread_join()`**:阻塞等待目标线程终止并回收资源,可获取退出码。未分离线程必须 join,否则会变成僵尸线程。线程之间关系对等,任意线程都能 join 另一个线程;**不能非阻塞调用**。 - **`pthread_detach()`**:把线程设为分离状态,线程终止时系统自动回收资源,之后不能再 `join`,且不可逆。适合不关心返回值的"即发即忘"线程。 - **`pthread_cancel()`**:向线程发送取消请求,**立即返回,不等待线程退出**。默认行为如同 `pthread_exit(PTHREAD_CANCELED)`。线程可设置 `PTHREAD_CANCEL_DISABLE` 不被取消,取消类型分 `DEFERRED`(到取消点才响应)和 `ASYNCHRONOUS`(任意时刻)。 区别总结:`join` 是"等待并回收",`detach` 是"让系统自动回收",`cancel` 是"请求对方退出"。 ### Q4:互斥锁、自旋锁、读写锁、条件变量各适合什么场景? **答**: | 机制 | 特点 | 适用场景 | | ---- | ---- | -------- | | 互斥锁 | 获取失败阻塞休眠,通用 | 一般临界区保护,使用最广 | | 条件变量 | 无状态,配合互斥锁,等待条件成立 | 生产者-消费者、事件通知 | | 自旋锁 | 获取失败原地自旋,不休眠,临界区必须极短 | 临界区极短;可用于中断上下文 | | 读写锁 | 读-读可并发,读-写/写-写互斥 | 读远多于写的共享数据 | 补充:互斥锁基于自旋锁实现;自旋锁占用 CPU,长时间自旋会降低效率;读写锁提高了读场景的并行性,但实现更复杂、写者可能饥饿。实际工程中用得最多的是互斥锁 + 条件变量组合。 ### Q5:什么是线程安全?什么是可重入?两者是什么关系? **答**: - **线程安全函数**:被多个线程同时调用时总能产生正确结果。可通过加锁等同步技术实现。 - **可重入函数**:被同一进程的多个执行流(包括信号处理函数执行流)同时调用时总能产生正确结果。要求更严格,通常要求只使用局部变量、参数和返回值为值类型、调用的其它函数也可重入。 - **关系**:可重入函数**一定是**线程安全函数;线程安全函数**不一定是**可重入函数(因为它可能修改外部全局变量,只是用锁保护了)。可重入是线程安全函数的真子集。 - 再严格一层是**异步信号安全函数**(可在信号处理函数中安全调用),可重入函数一定是异步信号安全函数。 - 实践:用 `man` 查 ATTRIBUTES,`MT-Safe` 表示线程安全、`MT-Unsafe` 表示线程不安全;有 `_r` 后缀的通常可重入。一个安全的信号处理函数只能调用异步信号安全函数(如 `write`),不能调用 `printf`、`malloc` 等。 --- **内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十一、十二章