|
@@ -0,0 +1,1987 @@
|
|
|
|
|
+---
|
|
|
|
|
+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 <pthread.h>
|
|
|
|
|
+#include <stdio.h>
|
|
|
|
|
+
|
|
|
|
|
+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);` |
|
|
|
|
|
+| 头文件 | `<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()` 开始运行。调用后无法确定系统先调度主线程还是新线程,若程序对执行顺序有强制要求,必须使用同步技术。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+编译运行:
|
|
|
|
|
+
|
|
|
|
|
+```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);` |
|
|
|
|
|
+| 头文件 | `<pthread.h>` |
|
|
|
|
|
+| 参数 `retval` | 线程返回值(退出码),可由 `pthread_join()` 获取 |
|
|
|
|
|
+| 返回值 | 无 |
|
|
|
|
|
+
|
|
|
|
|
+要点:
|
|
|
|
|
+
|
|
|
|
|
+- `pthread_exit()` 等价于在线程 start 函数中执行 `return`,区别是可在 start 函数所调用的**任意函数**中调用它来终止线程;
|
|
|
|
|
+- `retval` 所指向的内容**不应分配在线程栈中**(线程终止后栈内容是否有效不确定),start 函数的返回值同理;
|
|
|
|
|
+- 如果主线程调用 `pthread_exit()`,主线程终止,但其它线程仍正常运行,直到所有线程都终止进程才终止。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 1.7 回收线程:pthread_join()
|
|
|
|
|
+
|
|
|
|
|
+类似父进程用 `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`)。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 1.8 取消线程:pthread_cancel()
|
|
|
|
|
+
|
|
|
|
|
+有时需要向线程发送请求,要求它立刻退出,称为**取消线程**。
|
|
|
|
|
+
|
|
|
|
|
+| 项 | 内容 |
|
|
|
|
|
+| -- | ---- |
|
|
|
|
|
+| 原型 | `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);` 手动产生一个取消点:若线程已有挂起的取消请求,调用该函数后线程立即终止。
|
|
|
|
|
+
|
|
|
|
|
+#### 取消示例
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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()` 后又能被取消。
|
|
|
|
|
+
|
|
|
|
|
+### 1.9 分离线程:pthread_detach()
|
|
|
|
|
+
|
|
|
|
|
+默认线程终止时可由其它线程 `pthread_join()` 获取状态并回收资源。若不关心返回值、希望线程终止时系统**自动回收**,可将其**分离**。
|
|
|
|
|
+
|
|
|
|
|
+| 项 | 内容 |
|
|
|
|
|
+| -- | ---- |
|
|
|
|
|
+| 原型 | `int pthread_detach(pthread_t thread);` |
|
|
|
|
|
+| 头文件 | `<pthread.h>` |
|
|
|
|
|
+| 参数 | 需要分离的线程 ID |
|
|
|
|
|
+| 返回值 | 成功返回 `0`;失败返回错误码 |
|
|
|
|
|
+
|
|
|
|
|
+要点:
|
|
|
|
|
+
|
|
|
|
|
+- 线程既可以分离别的线程,也可以分离自己:`pthread_detach(pthread_self());`
|
|
|
|
|
+- 一旦处于分离状态,就**不能再使用 `pthread_join()`** 获取其终止状态,且此过程不可逆;
|
|
|
|
|
+- 处于分离状态的线程终止后,系统自动回收其资源。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 1.10 清理处理函数:pthread_cleanup_push/pop
|
|
|
|
|
+
|
|
|
|
|
+类似进程用 `atexit()` 注册终止处理函数,线程退出时也可执行**线程清理函数**。与进程不同,一个线程可以注册多个清理函数,记录在**清理函数栈**中(先进后出),执行顺序与注册顺序相反,全部执行完线程才终止。
|
|
|
|
|
+
|
|
|
|
|
+| 项 | 内容 |
|
|
|
|
|
+| -- | ---- |
|
|
|
|
|
+| 原型 | `void pthread_cleanup_push(void (*routine)(void *), void *arg);`<br/>`void pthread_cleanup_pop(int execute);` |
|
|
|
|
|
+| 头文件 | `<pthread.h>` |
|
|
|
|
|
+| `routine` | 清理函数指针,无返回值,只有一个 `void *` 参数 |
|
|
|
|
|
+| `arg` | 调用清理函数 `routine()` 时传给它的参数 |
|
|
|
|
|
+| `execute` | 为 `0` 只移除栈顶清理函数不执行;非 `0` 则移除并执行该函数 |
|
|
|
|
|
+
|
|
|
|
|
+清理函数栈中的函数在以下情况被执行:
|
|
|
|
|
+
|
|
|
|
|
+- 线程调用 `pthread_exit()` 退出时;
|
|
|
|
|
+- 线程响应取消请求时;
|
|
|
|
|
+- 用**非 0**参数调用 `pthread_cleanup_pop()` 时。
|
|
|
|
|
+
|
|
|
|
|
+除此之外,其它终止方式**不会**执行清理函数,例如在线程 start 函数中执行 `return` 退出时不会执行。
|
|
|
|
|
+
|
|
|
|
|
+注意:`pthread_cleanup_push()` 和 `pthread_cleanup_pop()` 实际是**宏**,会展开为 `{` 和 `}` 包裹的语句序列,因此必须在与线程相同的作用域中**成对匹配**使用,否则编译报错。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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`,则不会执行清理函数。
|
|
|
|
|
+
|
|
|
|
|
+### 1.11 线程属性
|
|
|
|
|
+
|
|
|
|
|
+创建线程时可通过 `pthread_attr_t` 设置属性;`attr` 为 `NULL` 表示全部使用默认值。若自定义属性,需先用 `pthread_attr_init()` 初始化、用完用 `pthread_attr_destroy()` 销毁。
|
|
|
|
|
+
|
|
|
|
|
+| 函数 | 原型 |
|
|
|
|
|
+| ---- | ---- |
|
|
|
|
|
+| 初始化/销毁属性 | `int pthread_attr_init(pthread_attr_t *attr);`<br/>`int pthread_attr_destroy(pthread_attr_t *attr);` |
|
|
|
|
|
+| 栈地址与大小 | `int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);`<br/>`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);`<br/>`int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);` |
|
|
|
|
|
+| 单独设置栈地址 | `int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);`<br/>`int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);` |
|
|
|
|
|
+| 分离状态 | `int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);`<br/>`int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);` |
|
|
|
|
|
+
|
|
|
|
|
+`detachstate` 取值:
|
|
|
|
|
+
|
|
|
|
|
+- `PTHREAD_CREATE_DETACHED`:新建线程一开始便处于分离状态,结束后由系统回收资源,无法被 `pthread_join()`;
|
|
|
|
|
+- `PTHREAD_CREATE_JOINABLE`:默认值,正常启动,可被其它线程获取终止状态。
|
|
|
|
|
+
|
|
|
|
|
+设置栈大小为 4 KB 的示例:
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+以分离状态启动线程:
|
|
|
|
|
+
|
|
|
|
|
+```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 <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`,互不影响。
|
|
|
|
|
+
|
|
|
|
|
+#### 可重入函数(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.h>` |
|
|
|
|
|
+| 初始化 | `pthread_once_t once_control = PTHREAD_ONCE_INIT;` |
|
|
|
|
|
+| 返回值 | 成功返回 `0`;失败返回错误编码 |
|
|
|
|
|
+
|
|
|
|
|
+若一个线程调用 `pthread_once()` 时另一个线程也调用,后者会阻塞等待,直到第一个完成初始化返回。调用成功返回时,可确定所有状态都已初始化完成。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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()` 只会被执行一次。
|
|
|
|
|
+
|
|
|
|
|
+#### 线程特有数据(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 <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
|
|
|
|
|
+
|
|
|
|
|
+线程局部存储比线程特有数据更简单:在全局或静态变量声明时加 `__thread` 修饰符,每个线程就都拥有一份该变量的拷贝,一直存在到线程终止时自动释放。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+static __thread char buf[512];
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+注意事项:
|
|
|
|
|
+
|
|
|
|
|
+- 变量声明中若用了 `static` 或 `extern`,`__thread` 必须紧随其后;
|
|
|
|
|
+- 与一般全局/静态变量一样,声明时可设初始值;
|
|
|
|
|
+- 可用取值操作符 `&` 获取线程局部变量地址。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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` 地址不同,证明它们操作的是各自线程的副本。
|
|
|
|
|
+
|
|
|
|
|
+### 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);` |
|
|
|
|
|
+| 头文件 | `<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()` 仅为了打印方便,实际项目中不应这样用。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 2. 线程同步
|
|
|
|
|
+
|
|
|
|
|
+### 2.1 为什么需要线程同步
|
|
|
|
|
+
|
|
|
|
|
+线程同步是为了对**共享资源**的访问进行保护,解决**数据一致性**问题。
|
|
|
|
|
+
|
|
|
|
|
+- 若每个线程访问的变量都是其它线程不会读写的(如各自局部变量、只有一个线程访问的全局变量),不存在一致性问题;
|
|
|
|
|
+- 变量只读时,多线程同时读取也没有问题;
|
|
|
|
|
+- 当一个线程可修改的变量,其它线程也能读取或修改时,就存在一致性问题,需要同步。
|
|
|
|
|
+
|
|
|
|
|
+本质原因是多个线程**并发访问**共享资源形成**竞态(race)**,可能导致读写到无效值。例如线程 A 读变量、再写新值,写操作需两个时钟周期,线程 B 在中间读取,就会读到不一致的值。
|
|
|
|
|
+
|
|
|
|
|
+用两个线程各递增全局变量 1000 万次来说明:期望结果 2000 万,实际结果往往小于它。
|
|
|
|
|
+
|
|
|
|
|
+```c
|
|
|
|
|
+#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 万,这就是数据不一致。
|
|
|
|
|
+
|
|
|
|
|
+```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);` |
|
|
|
|
|
+| 头文件 | `<pthread.h>` |
|
|
|
|
|
+| 参数 `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);`<br/>`int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);` |
|
|
|
|
|
+| 获取/设置类型 | `int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);`<br/>`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 <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()` 唤醒消费者,消费者被唤醒后自动重新加锁并消费。
|
|
|
|
|
+
|
|
|
|
|
+```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 <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` 是绝对时间,必须先取当前时间再加上超时时长。
|
|
|
|
|
+
|
|
|
|
|
+### 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 <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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+对比可见,替换为自旋锁后程序运行耗时明显变短,但要注意使用场景。
|
|
|
|
|
+
|
|
|
|
|
+### 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 <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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 2.5.3 读写锁属性
|
|
|
|
|
+
|
|
|
|
|
+读写锁只有一个属性——**进程共享属性**,与互斥锁、自旋锁相同。
|
|
|
|
|
+
|
|
|
|
|
+| 函数 | 原型 |
|
|
|
|
|
+| ---- | ---- |
|
|
|
|
|
+| 初始化/销毁属性 | `int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);`<br/>`int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);` |
|
|
|
|
|
+| 获取/设置共享属性 | `int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);`<br/>`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 <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` 并广播,最后等消费者退出,保证不丢数据 |
|
|
|
|
|
+
|
|
|
|
|
+编译运行:
|
|
|
|
|
+
|
|
|
|
|
+```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应用编程指南》第十一、十二章
|