一、线程的概念
线程是轻量级的进程,线程属于某个进程。一个进程可以有多个线程。
1. 进程与线程相同点
| 相同点 | 说明 |
|---|---|
| 都能实现并发 | 进程和线程都是并发执行的单位,可同时推进多个任务 |
| 都是系统调度/管理的对象 | 都由内核创建、调度、销毁,都有生命周期 |
| 都可用于处理耗时任务 | 把耗时操作放到子进程/子线程,避免阻塞主执行流 |
2. 进程与线程不同点
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 系统角色 | 系统中最小的资源分配单位 | 系统中最小的执行单位 |
| 包含关系 | 包含一个或多个线程,默认有一个主线程 | 属于某个进程,线程之间是平级关系 |
| 资源 | 资源独立(各自独立地址空间) | 共享所属进程的资源 |
| 栈区 | 独立地址空间 | 有各自独立的栈区,栈区不共享 |
| 稳定性 | 相对稳定,一个进程崩溃不影响其他进程 | 不稳定,多线程中一个线程崩溃,整个进程崩溃 |
| 创建开销 | 大,新建进程需创建约 3GB 空间 | 小,只需在进程空间中开辟新栈区(约 8MB) |
| 并发度 | 较低 | 更高 |
| 通信/共享 | 资源隔离,需 IPC(管道、共享内存等) | 直接读写共享变量,方便但需加锁同步 |
| 切换开销 | 大(涉及地址空间切换) | 小(同一地址空间内切换 |
3. 线程编程的步骤 POSIX
POSIX 是操作系统接口标准,POSIX 线程(pthread) 就是遵循该标准的线程 API。
1> pthread_create 创建 →
2> 线程函数里操作自己的栈区和共享资源 →
3> pthread_join / pthread_detach 回收资源。因为默认是 joinable 状态,线程退出后栈区不会自动释放,必须 join 或 detach,否则资源泄漏。
3.1 什么是POSIX
POSIX=PortableOperatingSystemInterface(可移植操作系统接口),是IEEE 制定的一套标准,规定操作系统应该提供哪些接口(系统调用、库函数、线程 API 等)。
| 要点 | 说明 |
|---|---|
| POSIX 是“标准”,不是“实现” | 它只规定“接口长什么样、行为如何”,具体由各系统实现。 |
| 谁遵循它 | Linux、Unix、macOS 基本都遵循;Windows 原生不遵循(但有兼容层)。 |
| 线程部分叫 POSIX Threads | 简称pthread,就是我们用的pthread_create、pthread_join这一套。 |
| 好处 | 按 POSIX 写的多线程代码,在 Linux/Unix/macOS 上基本能直接移植。 |
所以“POSIX 线程编程步骤”= 用pthread 标准 API写多线程的流程。
4. linux中查看进程的相关命令
4.1 ps -eLo pid,ppid,lwp,stat,comm
ps -eLo pid,ppid,lwp,stat,comm| 字段 | 含义 | 详细解释 |
|---|---|---|
| PID | Process ID | 进程 ID。代表这个线程属于哪个进程。 |
| PPID | Parent Process ID | 父进程 ID。创建这个进程的上层进程 ID。 |
| LWP | Light Weight Process | 线程 ID(轻量级进程 ID)。 在这个输出里,如果同一个 PID 下出现了多行,且 LWP 不同,就说明这是一个多线程程序。 |
| STAT | Process State | 进程/线程状态(这是这个命令最大的亮点)。详见下方扩展。 |
| COMM | Command | 命令名称。只显示可执行文件的名字,不显示路径和参数(最长 15 个字符)。 |
常见基础状态:
R (Running/Runnable):运行中或正在等待 CPU 时间片。
S (Sleeping):可中断的睡眠状态(比如在等网络、等键盘输入、等锁)。最常见的状态。
D (Disk Sleep):不可中断的睡眠状态(通常在等待磁盘 I/O,此时连 kill -9 都杀不掉)。
T (Stopped):暂停状态(比如被 SIGSTOP 信号暂停,或在调试器中被断点卡住)。
Z (Zombie):僵尸状态(进程已经结束,但父进程还没回收它)。
X (Dead):彻底死亡。
常见附加修饰符(跟在基础状态后面):
<:高优先级。
N:低优先级。
l:多线程(如果是进程级别的状态,带 l 说明它有子线程)。
s:会话首进程(Session leader)。
+:位于前台进程组。
4.2 ps -eLf,效果如下:
ps -eLf ps:Process Status,查看进程状态。 -e:代表 Every,显示系统中所有的进程(等同于 -A)。 -L:代表 LWP (Light Weight Process,轻量级进程),加上这个参数就会显示线程信息。 -f:代表 Full,以全格式显示详细信息(包含 UID, PID, PPID 等)。| 列名 | 全称 / 含义 | 详细解释 |
|---|---|---|
| UID | User ID | 运行该进程/线程的用户 ID(如图片中的 root)。 |
| PID | Process ID | 进程 ID,系统分配给该进程的唯一标识符。 |
| PPID | Parent Process ID | 父进程 ID,创建该进程的上一个进程的 ID(如 PID 为 3 的父进程是 2)。 |
| LWP | Light Weight Process | 轻量级进程 ID,实际上就是线程 ID。 注意:对于单线程进程,LWP 通常等于 PID;对于多线程进程,主线程的 LWP 等于 PID,其他子线程的 LWP 是系统另行分配的编号。 |
| C | CPU Utilization | CPU 利用率,表示进程最近使用的 CPU 百分比(偶尔用来参考,精确度不如top)。 |
| NLWP | Number of LWPs | 线程数量,表示该进程内当前共有多少个线程。 如图都是 1,说明这些都是单线程进程或内核线程;如果是普通的多线程程序,这里会显示 > 1 的数字。 |
| STIME | Start Time | 启动时间,进程开始运行的时间或日期(图中是 10月16日)。 |
| TTY | Teletype / Terminal | 终端设备,该进程依附的终端。?表示没有控制终端(通常是系统守护进程/内核进程)。 |
| TIME | CPU Time | 该进程累计占用的CPU 总时间(格式为分:秒)。 |
| CMD | Command | 命令名称,启动该进程的命令或程序名。 |
二、线程的创建
1. 函数:pthread_create();创建线程
注意回调函数,第三个参数。
pthread_t tid; 定义线程号:用于保存新创建线程的 ID,pthread_join/detach需使用 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); pthread_t *thread 出参:指向 pthread_t 变量的指针。 调用成功后,内核把新线程的 ID 写入 *thread(即 &tid)。 必须传有效地址,不能传 NULL。 const pthread_attr_t *attr 入参:线程属性(栈大小、分离状态、调度策略等)。 传 NULL 表示使用默认属性(可结合、默认栈大小、默认调度)。 需要定制时先 pthread_attr_init 再传入。 void *(*start_routine) (void *) 入参:线程入口函数指针,即线程启动后执行的函数。 原型必须严格是 void *func(void *),参数和返回值都是 void*。 返回值可被 pthread_join 的第二个参数接收。 void *arg 入参:传给 start_routine 的唯一参数,类型为 void*。 需要传多个值时可传结构体指针(注意生命周期/内存管理)。 不需要参数时传 NULL。 返回值:成功返回 0;失败返回错误码(非 errno), 如 EAGAIN(资源不足)、EINVAL(属性非法)、EPERM(无权限)。 注意:pthread 函数返回的是错误码本身,不要用 perror,应 strerror(ret)。2. 函数:pthread_self();获取线程号
pthread_t tid; 定义变量接收当前线程的 ID (pthread_t 在 Linux 下本质是 unsigned long int) pthread_t pthread_self(void); 返回值: 成功:返回当前线程的线程 ID (pthread_t 类型)。 失败:无。POSIX 标准规定该函数永远成功(因为它只是读取自身的 TCB 数据,无失败可能)。 打印提示:因为 pthread_t 底层通常是 unsigned long int,打印时建议强制转换并搭配 %lu: printf("%lu\n", (unsigned long)pthread_self()); void 入参:void(无参数)。 不需要传任何东西,它只针对“调用它的那个线程”自己生效。三、线程的退出和回收
1. 线程自行退出
void pthread_exit(void *retval); void *retval 入参:线程退出时的返回状态("临死遗言")。 可以通过强转传整数,如 (void *)1。 也可以传 malloc 分配的堆内存地址。 【大坑】绝不能传线程内部栈区局部变量的地址! 因为线程退出后栈区会被释放,主线程 join 拿到的是野指针。 返回值:无(该函数调用后线程即终止,不会返回到调用者)。 对比:exit() 是终止整个进程(所有线程),return 只能在主函数或线程函数里用。 如果在子线程中调用 exit(),整个程序都会挂掉!2. 请求取消线程
int pthread_cancel(pthread_t thread); pthread_t thread 入参:要取消的目标线程的 ID。 返回值:成功返回 0;失败返回错误码(非0)。 【重要机制】:pthread_cancel 只是“提出请求”,并不是立刻“强杀”。 目标线程必须运行到“取消点”(Cancellation Point,如 sleep, read, write 等系统调用,或主动调用 pthread_testcancel)才会真正退出。 如果目标线程是一个没有取消点的死循环(比如 while(1){}),它是不会理睬这个取消请求的。3. 阻塞回收
int pthread_join(pthread_t thread, void **retval); pthread_t thread 入参:要回收(等待)的目标子线程 ID。 void **retval 出参:二级指针,用于接收子线程退出的返回值(即 pthread_exit 或 return 传出的东西)。 如果只传 NULL,表示只回收资源,不关心返回值。 【用法示例】: void *ret = NULL; pthread_join(tid, &ret); // 此时 ret 就拿到了子线程的遗言。 返回值:成功返回 0;失败返回错误码(非0)。 【注意】: 1. 这是阻塞函数,调用它的线程(通常是主线程)会一直卡在这里,直到目标线程结束。 2. 一个线程只能被 join 一次。 3. 线程不能 join 自己。 4. 已经被 pthread_detach 分离的线程,不能再 join。4. 设置分离属性
int pthread_detach(pthread_t thread); pthread_t thread 入参:要设置为分离状态的目标线程 ID。 如果在子线程内部想分离自己,填 pthread_self() 即可。 返回值:成功返回 0;失败返回错误码(非0)。 【作用】:告诉系统:“这个线程死掉后,它的资源(如 8MB 的栈区)你直接帮我回收,不用等别人 join”。 【注意】: 1. 分离属性一旦设置,就无法再改回可结合(joinable)状态。 2. 如果设置分离,主线程就不能再调用 pthread_join,否则会报错(EINVAL)。 3. 常用来处理“不需要关心运行结果、跑完拉倒”的后台任务。四、线程控制
1. 互斥
概念:在多线程中对临界资源的排他性访问。
临界资源:多线程中会被读写操作的资源(全局变量、文件、设备)。
排他访问:同一时刻只能有一个线程进行读写操作。
为什么需要:多线程并发穿插执行时,像 A++ 这种操作在汇编层面至少是 3 句指令。如果线程 1 执行一半被切走,线程 2 接着执行,会导致数据一致性问题(竞态条件)。
使用步骤:定义锁 -> 初始化锁 -> 加锁 -> 解锁 -> 销毁。
1.1 互斥相关函数
1. 定义互斥锁 pthread_mutex_t mutex; 2. 初始化锁 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); 功能:将已经定义好的互斥锁初始化。 参数:mutex 要初始化的互斥锁。 attr 初始化的值,一般是 NULL 表示默认锁。 返回值:成功 0;失败 非零。 3. 加锁 int pthread_mutex_lock(pthread_mutex_t *mutex); 功能:用指定的互斥锁开始加锁代码。 加锁后的代码到解锁部分属于【原子操作】,在加锁期间其他线程不能操作该部分代码。 如果 mutex 已经被其他线程使用,则该函数会【阻塞】。 参数:mutex 用来给代码加锁的互斥锁。 返回值:成功 0;失败 非零。 4. 解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex); 功能:将指定的互斥锁解锁。 解锁之后代码不再排他访问,一般加锁解锁成对出现。 参数:mutex 用来解锁的互斥锁。 返回值:成功 0;失败 非零。 5. 销毁锁 int pthread_mutex_destroy(pthread_mutex_t *mutex); 功能:使用互斥锁完毕后需要销毁互斥锁。 参数:mutex 要销毁的互斥锁。 返回值:成功 0;失败 非零。2. 同步
概念:有一定先后顺序的对资源的排他性访问。
用途:在多线程中,让线程按照一定顺序运行。、
使用步骤:定义信号量 -> 初始化 -> PV 操作 -> 销毁。
2.1 同步与互斥的区别:
互斥包含同步,同步是互斥的特例。
操作线程:互斥锁的加锁和解锁必须是同一个线程;信号量(同步)是由线程交叉释放(th1 释放 th2,th2 释放 th1)。
临界区要求:互斥锁保护的代码(临界区)要求短小精悍,不能有休眠或大耗时操作;信号量适当可以有休眠或小的耗时操作。
计数信号量:信号量的初值可以大于 1(如 3、5),用于资源本身不唯一的互斥情况(多个相同资源)。
2.2 同步相关函数
1. 定义信号量 sem_t sem; 2. 初始化信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); 功能:将已经定义好的信号量赋值。 参数:sem 要初始化的信号量。 pshared =0 表示线程间使用信号量;!=0 表示进程间使用。 value 信号量的初始值。一般无名信号量都是二值信号量(0 或 1)。 0 表示红灯(阻塞);1 表示绿灯(通行)。 返回值:成功 0;失败 -1。 3. P 操作(申请资源) int sem_wait(sem_t *sem); 功能:判断当前 sem 是否有资源可用。 有资源(==1)则申请资源,程序继续运行; 没有资源(==0)则线程【阻塞等待】,一旦有资源则自动申请并继续运行。 注意:申请资源后会自动执行 sem = sem - 1。 参数:sem 要判断的信号量资源。 返回值:成功 0;失败 -1。 4. V 操作(释放资源) int sem_post(sem_t *sem); 功能:将指定的 sem 信号量资源释放。 默认执行 sem = sem + 1;线程在该函数上【不会阻塞】。 参数:sem 要释放资源的信号量。 返回值:成功 0;失败 -1。 5. 销毁信号量 int sem_destroy(sem_t *sem); 功能:使用完毕将指定的信号量销毁。 参数:sem 要销毁的信号量。 返回值:成功 0;失败 -1。3. 死锁(Deadlock)
概念:由于锁资源安排得不合理(锁资源的申请和释放逻辑不对),导致进程或线程无法正常继续执行(推进)的现象。
产生死锁的四个必要条件(必须同时满足才会发生死锁):
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
五、注意事项
1. 资源与内存
1.1 栈独立,堆共享:独立栈区存局部变量。堆、全局/静态变量、文件描述符由所有线程共享,多线程读写共享数据必须加锁(如 pthread_mutex_t)。
1.2 传参生命周期:arg 为 void*。避免传局部变量地址,防止主线程提前退出或变量失效;推荐:malloc 结构体传参,或将值直接强转指针 (void*)100。
1.3 返回值生命周期:pthread_exit 和 return 的返回指针,禁止指向线程局部变量(栈区销毁会变野指针);推荐:malloc 堆内存返回,主线程 join 后 free;或直接强转值返回 (void*)1。
1.4 禁用 exit:子线程调用 exit() 会终止整个进程。子线程退出只能用 pthread_exit 或 return。
2. 核心 API 细节
2.1 pthread_create 返回值:成功返回 0,失败返回错误码(非 -1),不修改 errno。需用 strerror(ret),不能用 perror。
2.2 pthread_self 打印:Linux 下 pthread_t 为无符号长整型,打印需强转:printf("%lu", (unsigned long)pthread_self());
2.3 pthread_cancel 取消点:发送取消请求,目标线程必须运行至“取消点”(如 sleep, read, pthread_testcancel)才会退出。无取消点的纯死循环无法被取消。
2.4 pthread_join 约束:阻塞等待;只能 join 一次;不能自 join;不能 join 已 detach 的线程。
3. 生命周期与回收
3.1 默认属性(joinable):线程退出后栈区和控制块不自动释放。不 join 也不 detach 会导致僵尸线程(内存泄漏)。
3.2 pthread_detach:设置分离后,线程退出时系统自动回收资源;不可逆:分离后绝不能再次 join。
3.3 主线程退出:主线程 return 或 exit() 会强杀所有子线程。需等待子线程时,必须用 pthread_join 或 pthread_exit(NULL)。
4. 编译与调试
4.1 编译选项:必须加 -pthread:gcc test.c -o test -pthread
4.2 ps 查看线程
ps -eLf:看 NLWP(线程数)、LWP(线程ID)。
ps -eLo pid,ppid,lwp,stat,comm:自定义状态。主线程 LWP = PID;子线程 LWP 独立;STAT 状态:S(睡眠), R(运行), D(不可中断IO), Z(僵尸)。
5. 核心口诀
传参防野指针,返回必用 malloc;
退出千万别用 exit,死循环里加取消点;
建了线程必须收尸(join/detach),编译别忘 -pthread。