040 线程控制
🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022
文章目录
- 线程控制
- 1. POSIX 线程库
- 1. 什么是 POSIX 线程库(pthread)
- 2. 特点
- 3. pthread_t
- 1. 功能
- 2. 本质
- 4. pthread_create —— 创建新线程
- 1. 功能
- 2. 函数原型
- 3. 参数详解
- 4. 返回值
- 5. 代码示例
- 6. 线程监控与查看
- 1. `ps axj | head -1 & ps axj | grep pthread_create | grep -v grep`
- 2. `ps -aL | head -1 & ps -aL | grep pthread_create | grep -v grep`
- 3. `ldd pthread_create`
- 2. 线程的调度
- 1. 线程调度单位到底是谁?
- 2. 什么是 LWP(Light Weight Process)?
- 3. Linux 中 pthread 与 LWP 的关系?
- 4. 为什么说 “我们以前接触到的都是单线程进程,PID 和 LWP 相等”?
- 5. pthread_self —— 获取线程 ID
- 1. 功能
- 2. 函数原型
- 3. 返回值
- 4. 代码示例
- 3. 线程等待
- 1. 线程等待是什么?
- 2. pthread_join —— 阻塞线程
- 1. 功能
- 2. 函数原型
- 3. 参数详解
- 4. 返回值
- 5. 代码示例
- 3. 线程退出值 = 退出码(为啥只能拿这个)?
- 1. 为什么线程退出时只能“返回一个退出码”?
- 2. 为什么不是像 `fork()` 那样返回整型或 exit code?
- 4. 线程终止
- 1. 线程终止的 4 种方式(核心)
- 2. `pthread_exit` —— 主动退出当前线程
- 1. 功能
- 2. 函数原型
- 3. 参数详解
- 4. 返回值
- 5. 代码示例
- 3. `pthread_cancel` —— 发送请求
- 1. 功能
- 2. 函数原型
- 3. 参数详解
- 4. 返回值
- 5. 注意事项
- 6. 代码示例
- 4. 小结
- 5. 线程分离
- 1. 线程分离是什么,有什么用?
- 2. `pthread_detach` 函数
- 1. 功能
- 2. 函数原型
- 3. 参数说明
- 4. 返回值
- 5. 代码示例
- 3. 线程分离的本质是什么?
- 4. 小结
- 6. C++ 语言层面上的多线程支持(C++11 起,了解)
- 1. pthread VS std:: thread
- 2. `std::thread` vs `pthread` 对照表
- 3. 代码示例
- 1. 创建线程
- 2. 等待线程结束:`join()`
- 3. 分离线程:`detach()`
- 4. 获取线程 ID
- 5. 支持 lambda(超方便!)
- 6. 支持类成员函数
- 7. 可重入与线程安全
- 1. 代码示例:一个不可重入的函数
- 2. 可重入函数应该是什么样?
- 3. 小结
- 8. 代码实战
- 1. 多种终止方式
- 2. 多线程的协同
- 3. 多线程特性综合演示
- 共勉
线程控制
1. POSIX 线程库
1. 什么是 POSIX 线程库(pthread)
POSIX(Portable Operating System Interface)线程库,又称(简称) pthread(POSIX Threads),是 Unix 系统下的标准化多线程编程接口(IEEE POSIX 标准(IEEE 1003.1c)定义的线程接口)。它提供了一组函数,用于在同一进程内创建、管理和同步多个线程,实现并发和并行处理。
pthread 线程库是应用层的原生线程库: 应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。大部分 Linux 系统都会默认带上该线程库(原生的)。与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 pthread_
打头的。要使用这些函数库,要通过引入头文件 <pthreaad.h>
,链接这些线程函数库时,要使用编译器命令的 -lpthread
选项。
2. 特点
- 与操作系统紧密集成,性能开销小。
- 接口统一,可移植性好。
- 支持线程同步(互斥锁、条件变量)、线程属性设置等丰富功能。
- 线程共享同一进程的内存空间(代码段、堆、全局变量等)。
- 线程间通信更高效(直接访问共享数据)。
- 适用于需要高并发的场景(如服务器、实时处理)。
3. pthread_t
1. 功能
pthread_t
是 pthread 库定义的线程标识类型,类似于进程中的 PID。每个创建的线程都会分配一个唯一的 pthread_t
值,用来引用和管理该线程。
2. 本质
Linux 下,pthread_t
通常以整数或指针的形式存在(与 glibc 实现有关),我们只需将其当作“黑盒”标识符,配合其他 pthread 函数即可。
#include <pthread.h> // 声明 pthread_t 类型
pthread_t tid; // 声明一个线程标识符,创建后,tid 中存储的是新线程的 ID
4. pthread_create —— 创建新线程
1. 功能
创建一个新线程,并让它去执行用户定义的函数。
2. 函数原型
#include <pthread.h>int pthread_create(pthread_t *thread, // 输出参数:返回新线程 ID(pthread_t类型)const pthread_attr_t *attr, // 线程属性,NULL 表示默认void *(*start_routine)(void *), // 线程入口函数,即线程启动后要执行的函数(必须接受void*参数并返回void*)void *arg // 传给入口函数 start_routine 的参数,即线程(要执行)函数的参数
);
3. 参数详解
thread
:指向pthread_t
的指针,函数返回后通过它获取新线程的 ID;attr
:线程属性指针,可设置线程栈大小、分离状态等,通常传NULL
/nullptr
;start_routine
:线程函数指针,必须形如void* func(void*)
;arg
:传给线程函数的单个参数,可是任意指针(如结构体、基本类型地址),需要在函数内强转回原类型。
4. 返回值
POSIX 线程(pthreads)函数在出错时 不设置全局 errno ,而是 直接通过返回值返回错误码 (成功为 0,失败为非 0 值)。这与传统系统调用(如 open、read)不同,传统调用通常返回-1 并设置 errno。pthreads 这样做是为了避免多线程环境下对全局 errno 的竞争,提升性能和可移植性。尽管每个线程有独立的 errno 以兼容其他使用它的代码,但 建议始终检查 pthreads 函数的返回值来判断错误 ,而不是依赖 errno。
- 返回
0
:创建成功; - 返回非
0
:错误码,表示创建失败(如资源不足、权限问题等)。
5. 代码示例
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void* func(void* arg)
{char* str = (char*)arg;while(1){cout << str << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, func, (void*)"我是子线程,ID是:123456");while(1){cout << "我是主线程!" << endl;sleep(1);}return 0;
}
编译(-lpthread):
-
编译命令
g++ -o pthread_create pthread_create.cc -lpthread
-lpthread
:链接 pthread 库,必须加在源文件或对象文件之后。 -
makefile 文件:
pthread_create:pthread_create.ccg++ -o $@ $^ -std=c++11 -lpthread .PHONY: clean clean:rm -f pthread_create
运行结果示例:
原子性 指一个操作要么 完全执行成功 ,要么 完全没执行 ,在执行过程中 不会被中断或分割 。原子性 = 不可分割性,一个操作如果是原子的,就 不会被其他线程或中断打断 ,外界看起来就像“瞬间完成”。
6. 线程监控与查看
-
查看进程和线程
-
ps -AL
:列出所有线程(Lightweight Process,LWP)ps -AL | grep pthread_create # 输出中,LWP 列就是线程 ID
-
-
查看可执行文件依赖
-
ldd
:列出可执行文件所依赖的共享库ldd pthread_create # 可以确认是否已正确链接 libpthread.so
-
-
结合 top 或 htop
- 在
top
中,按H
可切换到线程视图; - 便于实时监控各线程的 CPU/内存占用情况。
- 在
以下是命令的逐步解析:
1.
ps axj | head -1 & ps axj | grep pthread_create | grep -v grep
ps axj
:ps
是 Linux 系统中用于查看进程状态的命令。-a
:显示所有终端上的进程,包括其他用户的进程。-x
:显示没有控制终端的进程。-j
:以长格式显示线程信息,包括线程 ID、进程组 ID、会话 ID 等。head -1
:只取第一行,通常是表头信息。grep pthread_create
:过滤出包含字符串pthread_create
的行,这些行通常与使用了pthread_create
函数创建的线程相关。grep -v grep
:排除包含grep
自身的行,避免干扰。2.
ps -aL | head -1 & ps -aL | grep pthread_create | grep -v grep
ps -aL
:-a
:显示所有终端上的进程。-L
:显示线程信息,类似于-j
,但格式稍有不同。3.
ldd pthread_create
ldd
:显示指定可执行文件或共享库所依赖的动态链接库。
clone()
是 Linux 提供的底层系统调用,用于创建子进程或线程,是fork()
和pthread_create()
的核心实现基础之一。相比fork()
,它更灵活,可以通过传入不同的标志位来控制父子进程(或线程)之间是否共享地址空间、文件描述符、信号处理等资源,从而实现“线程”效果。因为使用较复杂(需要手动分配栈空间等),只做了解,不推荐直接使用。头文件:<sched.h>
,函数原型:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);
2. 线程的调度
1. 线程调度单位到底是谁?
“应用层的线程与内核的 LWP 是一一对应的,调度的是 LWP 而非 PID。”在 Linux 中:调度单位是 LWP(轻量级进程),而不是进程本身(PID)。
可以理解为:
概念 | 含义 |
---|---|
应用层线程 | 即 pthread_create 创建的线程 |
内核 LWP | 每个线程在内核中的调度实体 |
PID 与 LWP | 主线程的 LWP ID 与 PID 相等,子线程的 LWP ID 不等于 PID |
调度单位 | Linux 中调度的是每个线程(LWP),不是进程整体 |
2. 什么是 LWP(Light Weight Process)?
LWP 是 Linux 内核中的 最小调度单位,本质上就是一个“执行上下文”:包括程序计数器、栈、寄存器、调度信息等。每个 LWP 都有自己的 ID,即在 ps -AL
中看到的 LWP(Thread ID)。它们共享 所属进程的虚拟内存空间、打开的文件、信号处理器等资源。
3. Linux 中 pthread 与 LWP 的关系?
在 Linux 上,每个 pthread 线程 = 一个 LWP。
pthread_create()
创建的每一个线程,都会在内核中映射成一个 LWP;- 所以 Linux 是通过调度多个 LWP 来实现多线程程序并发运行(先描述再组织)。
4. 为什么说 “我们以前接触到的都是单线程进程,PID 和 LWP 相等”?
单线程程序只有一个主线程,所以它只有一个 LWP。而这个 LWP 的 ID(TID)刚好等于进程 ID(PID),getpid() == gettid()
(在主线程中成立)。但如果在程序中创建了多个线程(用 pthread),就会发现:
getpid()
(获取进程 ID)在每个线程中都一样;gettid()
(获取线程 ID)每个线程都不同;ps -AL
或top -H
会列出多个线程,每个线程一个 TID(内核调度单位);
系统调度的是 LWP(线程),不是进程(PID),这就是为什么:
- 多线程程序中,真正被调度运行的是每个线程(LWP)。
- 哪些线程先运行,哪些后运行,完全由调度器决定(不是你代码里的顺序)。
- 每个 LWP 都可能在不同的 CPU 核心上并发运行(多核 CPU)。
5. pthread_self —— 获取线程 ID
1. 功能
获取 当前线程自身的线程 ID(pthread 库中的 ID 类型 pthread_t
),用于线程内部识别自身,或与其他线程 ID 进行比较。
2. 函数原型
#include <pthread.h>
pthread_t pthread_self(void);
- 无参数,不需要传入任何值,它自动返回当前线程对应的
pthread_t
。 - 返回值类型是
pthread_t
,表示当前线程的 ID。
3. 返回值
返回 当前线程的 ID(类型为 pthread_t
),可以用这个 ID:
- 打印出来查看当前线程是谁;
- 与其他
pthread_t
对比,判断是不是同一个线程;注意:pthread_t
是个不透明类型,比较是否相等,应使用pthread_equal()
函数。 - 在调试或日志记录中标识线程身份。
4. 代码示例
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void* func(void* arg)
{pthread_t tid = pthread_self(); // 获取当前线程 IDcout << "子线程 pthread_self() = " << tid << endl;printf("这是子线程 pthread_t 变量的地址 %p\n", &tid);return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, func, nullptr);sleep(1);pthread_t main_tid = pthread_self(); // 主线程自身 IDcout << "主线程 pthread_self() = " << main_tid << endl;printf("这是主线程 pthread_t 变量的地址 %p\n", &main_tid);return 0;
}
运行结果示例:
[hcc@hcss-ecs-be68 Threads]$ ./pthread_self
子线程 pthread_self() = 140119442462464
这是子线程 pthread_t 变量的地址 0x7f7019980ef8
主线程 pthread_self() = 140119459862336
这是主线程 pthread_t 变量的地址 0x7ffebae7f9e0
主线程和子线程的 pthread_self()
返回值不同,说明它们是两个独立的线程。两个 tid
变量虽然名字类似,但位于不同线程的栈上。多线程环境下,函数的局部变量是线程安全的(自动隔离)。
3. 线程等待
1. 线程等待是什么?
在多线程程序中,主线程或其他线程可能需要等待某个线程执行完毕后再继续执行。这个等待的过程叫做 线程等待 。类似于进程中的 wait()
系统调用。
2. pthread_join —— 阻塞线程
1. 功能
阻塞当前线程,直到指定的线程结束,并可 获取该线程的返回值(退出码)。
- 常用于 主线程等待子线程完成任务;
- 可以在子线程中
return
或使用pthread_exit()
返回一个结果; - 主线程通过
pthread_join()
把这个返回值拿到。
2. 函数原型
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
3. 参数详解
参数名 | 类型 | 说明 |
---|---|---|
thread | pthread_t | 要等待的线程 ID,一般是 pthread_create 时返回的 |
retval | void** | 二级指针,接收线程退出时的退出码信息(可以为 NULL ) |
retval 的注意:
- 若不关心线程返回什么,可以传
NULL
/nullptr
; - 若关心,则要定义
void* result
,传&result
,线程退出时返回值会保存在result
中。
4. 返回值
- 成功:返回 0。
- 失败:返回非 0,即错误码(如无效的线程 ID、线程不存在等)。
5. 代码示例
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void* func(void* arg)
{int id = *(int*)arg; // 获取线程编号sleep(id); // 模拟不同耗时int* result = new int(id * 10); // 所有线程都要分配内存,让他们分别乘以 10cout << "子线程 " << id << " 完成任务,返回结果 = " << *result << endl;return (void*)result; // 返回退出码(指针)
}int main()
{pthread_t tids[5]; // 线程 ID 数组int ids[5]; // 用于传参的线程编号数组for (int i = 0; i < 5; i++) // 创建多个子线程{ids[i] = i + 1; // 编号从 1 开始pthread_create(&tids[i], nullptr, func, &ids[i]);}for (int i = 0; i < 5; i++) // 主线程等待所有子线程完成{void* retval = nullptr;pthread_join(tids[i], &retval); // 阻塞等待子线程退出int* res = (int*)retval; // 转换返回值cout << "主线程获取到线程 " << (i + 1) << " 的返回结果 = " << *res << endl;delete res; // 释放返回结果的内存}cout << "所有线程任务已完成,主线程退出。" << endl;return 0;
}
运行结果示例:
[hcc@hcss-ecs-be68 pthread_join]$ ./pthread_join
子线程 1 完成任务,返回结果 = 10
主线程获取到线程 1 的返回结果 = 10
子线程 2 完成任务,返回结果 = 20
主线程获取到线程 2 的返回结果 = 20
子线程 3 完成任务,返回结果 = 30
主线程获取到线程 3 的返回结果 = 30
子线程 4 完成任务,返回结果 = 40
主线程获取到线程 4 的返回结果 = 40
子线程 5 完成任务,返回结果 = 50
主线程获取到线程 5 的返回结果 = 50
所有线程任务已完成,主线程退出。
3. 线程退出值 = 退出码(为啥只能拿这个)?
pthread_join()
是主线程 阻塞等待 子线程完成,并 获取子线程返回值(退出码) 的唯一方式,而这个退出码只能是void*
类型,因为 POSIX pthread 模型就规定线程函数只能返回一个void*
指针。
1. 为什么线程退出时只能“返回一个退出码”?
因为线程函数的原型是:
void* (*start_routine)(void*)
- 它只能有一个返回值,类型是
void*
,这是 POSIX 标准设计决定的; - 无法直接返回多个值,也不能返回栈上对象(因为线程函数退出后栈就销毁了);
- 所以如果需要返回复杂数据,必须 动态申请内存(如
new
)并 return 指针,主线程通过pthread_join()
接收并自行释放。
2. 为什么不是像 fork()
那样返回整型或 exit code?
fork()
是 进程级别,操作系统可以记录 exit status;pthread_exit()
或return
是线程级别,线程之间共享地址空间,退出值不需要写入操作系统状态;pthread_join()
只关心线程退出时返回的那块 用户级别的数据指针(void*),而不是操作系统的退出码。
4. 线程终止
1. 线程终止的 4 种方式(核心)
在 POSIX pthread
中,线程的终止方式主要有以下几种:
-
线程函数运行完毕后自动返回: 这是最自然的退出方式,函数体执行到最后,线程就自动退出。
-
在函数中调用
pthread_exit()
主动退出: 这种方式适用于希望 中途退出线程,但又希望返回一个退出值的情况。可随时退出线程、能设置退出码、等效于return
。 -
其他线程调用
pthread_cancel()
强制取消线程: 一种 异步控制 方式。线程不一定立即退出,需要处于“可取消点”(如 sleep、read 等),线程退出码为PTHREAD_CANCELED ((void*)-1)
。注意:如果线程没有设置为可取消状态,pthread_cancel()
无效。 -
整个进程退出时,所有线程终止: 当 主线程调用
exit()
、_exit()
或主线程崩溃 导致整个进程终止时,所有线程也会强制结束。粗暴退出会导致线程无法清理资源,常见于崩溃或异常退出。
终止方式 | 触发者 | 是否能传退出码 | 是否立即退出 | 是否安全 |
---|---|---|---|---|
return | 本线程自己 | ✅ 是 | ✅ 是 | ✅ 推荐 |
pthread_exit() | 本线程自己 | ✅ 是 | ✅ 是 | ✅ 推荐 |
pthread_cancel() | 其他线程 | ✅(默认为 PTHREAD_CANCELED ) | ❌ 依赖可取消点 | ⚠️ 谨慎使用 |
exit() / 崩溃 | 任意线程 | ❌ 无法获取 | ✅ 是 | ❌ 不推荐 |
2. pthread_exit
—— 主动退出当前线程
1. 功能
主动退出当前线程,并返回一个退出值(给等待该线程的其他线程)。比 return
更灵活,可在任何地方终止线程。
适用于:
- 线程需要在函数中间提前退出;
- 想设置返回值供
pthread_join
获取;
2. 函数原型
#include <pthread.h>
void pthread_exit(void* retval);
3. 参数详解
void* retval
: 线程的退出码,可传任意指针或整型强转。是 返回给 pthread_join 的退出值,如果线程已分离,该值会被忽略。
4. 返回值
无返回值,调用后线程立即退出,后面的代码不会执行。
5. 代码示例
#include <iostream>
#include <pthread.h>
using namespace std;void* myThread(void* arg)
{cout << "子线程正在运行..." << endl;pthread_exit((void*)1234); // 主动退出,返回值是1234cout << "子线程退出已经退出了,并且这一句话不会被执行!" << endl;
}int main()
{pthread_t tid;void* retval;pthread_create(&tid, nullptr, myThread, nullptr);pthread_join(tid, &retval); // 获取子线程返回值cout << "子线程退出码:" << (long long)retval << endl;// 注意:在64位的Linux中,void*(指针)是 8 字节,所以转成 long long,如果转成 int 会报错!return 0;
}
运行结果示例:
[hcc@hcss-ecs-be68 pthread_exit]$ ./pthread_exit
子线程正在运行...
子线程退出码:1234
3. pthread_cancel
—— 发送请求
1. 功能
向指定线程发送“取消请求”,尝试强制终止它(但不一定立即终止)。
适用于:
- 主动终止长时间运行或卡死的线程;
- 线程之间的控制与协作场景。
2. 函数原型
#include <pthread.h>
int pthread_cancel(pthread_t thread);
3. 参数详解
pthread_t thread
: 目标线程的线程 ID。
4. 返回值
返回值 | 含义 |
---|---|
0 | 成功发送取消请求(注意:不是线程已退出!) |
ESRCH | 没有找到指定的线程 ID(线程不存在) |
EINVAL (少见) | 线程 ID 无效(某些实现中使用) |
5. 注意事项
- 不是立即强制终止线程!
- 被取消线程 必须处于可取消状态,且处于 取消点;
- 常见取消点有
sleep
、read
、pthread_join
等。
- 线程取消成功后,其退出值为
(void*)PTHREAD_CANCELED
,用来判断线程是否被取消。 - 如何查一个函数是不是取消点?
- 查阅 POSIX 官方文档
- 使用命令:
man 7 pthreads
,在 Linux 手册中输入/
搜索 “Cancellation points”,会列出所有标准取消点函数。
PTHREAD_CANCELED
是 POSIX 线程库(pthread)中一个 宏常量,它 用于判断/标识线程是否是被pthread_cancel()
强制取消退出的,而不是正常执行完毕返回的,可以在pthread_join()
后检查返回值是否等于它,来确认线程状态。其本质是个#define
(宏)#define PTHREAD_CANCELED ((void *) -1)
- 它是一个
void*
类型的宏常量;- 实际上是
(void*)-1
,特殊指针,用作标记;- 不能拿它当做真实的返回值内容使用,只能 判断它是不是等于某线程的退出码。
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std;void* func(void* arg) {while (1){cout << "子线程运行中..." << endl;sleep(1);} }int main() {pthread_t tid;pthread_create(&tid, nullptr, func, nullptr);sleep(2); // 让子线程跑一会儿pthread_cancel(tid); // 取消子线程void* retval;pthread_join(tid, &retval); // 等待子线程结束// 检查返回值是否为被取消if (retval == PTHREAD_CANCELED){cout << "子线程被成功取消,返回值 = PTHREAD_CANCELED" << endl;}else{cout << "子线程正常退出,返回值 = " << retval << endl;}return 0; }
[hcc@hcss-ecs-be68 pthread_cancel]$ g++ -o PTHREAD_CANCELED PTHREAD_CANCELED.cc -std=c++11 -lpthread [hcc@hcss-ecs-be68 pthread_cancel]$ ./PTHREAD_CANCELED 子线程运行中... 子线程运行中... 子线程被成功取消,返回值 = PTHREAD_CANCELED
6. 代码示例
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void* func(void* arg)
{while (true){cout << "子线程工作中..." << endl;sleep(1); // sleep 是一个可取消点// pthread_testcancel(); // 手动设置一个取消点// 注意:如果使用 pthread_testcancel(),在这个程序中会无限打印 "子线程工作中...",在3秒后会自动退出,// 和sleep(1)一样的效果。虽然看起来像是死循环,但实际上是可以取消的!}return nullptr;
}int main()
{pthread_t tid;void* retval;pthread_create(&tid, nullptr, func, nullptr);sleep(3); // 让子线程运行一会pthread_cancel(tid); // 发送取消请求pthread_join(tid, &retval); // 等待子线程结束并获取退出码if (retval == PTHREAD_CANCELED){cout << "收到取消请求,子线程被取消了!" << endl;}return 0;
}
输出:
[hcc@hcss-ecs-be68 pthread_cancel]$ ./pthread_cancel
子线程工作中...
子线程工作中...
子线程工作中...
收到取消请求,子线程被取消了!
4. 小结
函数 | 主体是谁调用 | 用于哪个线程 | 是否立即终止 | 是否能设置退出码 |
---|---|---|---|---|
pthread_exit | 当前线程 | 自己 | 立即 | 是 |
pthread_cancel | 外部线程 | 目标线程 | 非立即 | 返回值为宏 |
线程分离(Detached Thread)是 Linux 多线程(POSIX 线程 pthread
)编程中的一个重要概念。下面我们从概念入手,逐步深入 pthread_detach
函数及其背后机制,讲清楚线程分离的本质。
5. 线程分离
1. 线程分离是什么,有什么用?
在默认情况下(joinable 模式),线程执行完后不会立即释放资源,需要其他线程调用 pthread_join()
与之回收,才能释放其占用的资源(如线程栈、PCB 结构等)。
线程分离(detached) 就是让线程在执行完毕后自动释放自己的资源,不再需要其他线程去 pthread_join()
它,防止资源泄漏。注意:线程一旦结束,系统自动回收资源,不能再被 pthread_join
!
2. pthread_detach
函数
1. 功能
将一个线程设置为 分离状态,使其结束时资源自动释放。
2. 函数原型
#include <pthread.h>
int pthread_detach(pthread_t thread);
3. 参数说明
thread
:需要设置为分离状态的线程 ID(pthread_t
类型)。
4. 返回值
0
:成功。EINVAL
:线程不是 joinable 或状态无效。ESRCH
:指定的线程 ID 不存在。
5. 代码示例
#include <pthread.h>
#include <iostream>
#include <unistd.h>
using namespace std;void* func(void* arg)
{sleep(1);cout << "子线程开始执行..." << endl;cout << "子线程执行完毕,自动释放资源。" << endl;pthread_exit(nullptr);
}int main()
{pthread_t tid;// 创建线程(默认是 joinable 状态)if (pthread_create(&tid, nullptr, func, nullptr) != 0){perror("线程创建失败");return 1;}// 设置线程为分离状态if (pthread_detach(tid) != 0){perror("线程分离失败");return 1;}cout << "主线程不等待子线程,直接结束。" << endl;sleep(3);return 0;
}
运行结果示例:
[hcc@hcss-ecs-be68 pthread_detach]$ ./pthread_detach
主线程不等待子线程,直接结束。
子线程开始执行...
子线程执行完毕,自动释放资源。
3. 线程分离的本质是什么?
线程分离的本质就是线程的一个属性,叫作:
- PTHREAD_CREATE_JOINABLE(默认)
- PTHREAD_CREATE_DETACHED(分离)
这个属性决定了线程的生命周期如何管理资源:
- JOINABLE 状态:线程执行完还要别人
join()
回收资源; - DETACHED 状态:线程执行完直接自己清理干净,别人不能
join()
它。
一旦线程设置为分离状态,无法再对它调用 pthread_join()
,否则会导致未定义行为,分离线程是否继续执行不取决于主线程是否退出,而取决于进程是否还活着。(分离线程 = 自动回收,非分离线程 = 手动回收)。
4. 小结
- 如果创建的线程是 短生命周期、且不需要结果回传的(如后台异步日志写入),建议使用
pthread_detach()
或属性设置为分离。 - 如果需要线程返回值(如子线程计算结果后回传主线程),则必须使用
joinable
并调用pthread_join()
。 - 线程分离后,即使主线程退出,分离线程也不一定会结束,只有当进程退出才会影响!
- 千万不要创建完线程后忘记
join()
或detach()
,否则会发生资源泄漏,尤其在线程频繁创建时。
6. C++ 语言层面上的多线程支持(C++11 起,了解)
pthread
是 C 语言的 POSIX 线程库(第三方),可在 C++ 中直接使用,但并非 C++ 语言原生支持;从 C++11 起,C++ 在语言层面提供了 std::thread
作为标准多线程支持,具有更好的类型安全和跨平台性,底层在 Linux 上通常基于 pthread
实现,但对用户透明。
1. pthread VS std:: thread
概念 | 说明 |
---|---|
pthread | 是 POSIX 标准定义的 C 语言线程 API,不是 C++ 的一部分。在 Linux 上通过 <pthread.h> 提供。 |
std::thread | 是 C++11 标准引入的 C++ 原生线程类,属于 C++ 标准库,头文件 <thread> 。 |
对比 | pthread (C 风格) | std::thread (C++11 起标准库) |
---|---|---|
来源 | POSIX API(非 C++ 标准) | C++ 标准库(C++11 起) |
头文件 | <pthread.h> | <thread> |
依赖 | POSIX 系统(Linux/Unix),Windows 原生不支持 | 跨平台:Windows / Linux / macOS(编译器支持即可) |
语言风格 | C 风格:函数指针 + void* 参数 | C++ 风格:支持 lambda、成员函数、函数对象、模板 |
类型安全 | void* 传参,易出错 | 模板自动推导,类型安全 |
封装性 | 纯函数式调用,无类封装 | std::thread 是类,支持 RAII、移动语义 |
适合谁 | 系统编程、嵌入式、高性能定制、底层原理 | 应用开发、跨平台项目、现代 C++ 开发 |
退出机制 | pthread_exit() 、pthread_join() | t.join() 、t.detach() ,析构时自动检查 |
可移植性 | 仅限 POSIX 系统 | 只要编译器支持 C++11 就可移植 |
底层实现 | 直接调用内核 LWP(轻量级进程) | 在 Linux 上通常基于 pthread 封装(但对用户透明) |
编译选项 | g++ -o app app.cpp -lpthread | g++ -o app app.cpp -std=c++11 (自动链接) |
2. std::thread
vs pthread
对照表
pthread 函数 | std::thread 写法 | 说明 |
---|---|---|
pthread_create(&tid, nullptr, func, arg) | std::thread t(func, arg); | 创建线程 |
pthread_join(tid, &ret) | t.join(); | 等待线程结束 |
pthread_exit() | return; | 线程函数返回即退出 |
pthread_self() | std::this_thread::get_id() | 获取当前线程 ID |
sleep(1) | std::this_thread::sleep_for(1s); | 睡眠 1 秒 |
3. 代码示例
1. 创建线程
thread t(函数名, 参数...);
参数会自动拷贝(如果是对象)想传引用?用 std::ref(变量)
包一层。
void func(int& x) { x = 100; }
int val = 0;
thread t(func, ref(val)); // 传引用
t.join();
cout << val; // 输出 100
2. 等待线程结束:join()
t.join(); // 必须调用,否则程序会崩溃!
类比 pthread_join
,一个 thread
对象只能 join()
一次。
3. 分离线程:detach()
t.detach(); // 不等它,让它后台运行
4. 获取线程 ID
cout << "当前线程ID: " << this_thread::get_id() << endl;
cout << "t的线程ID: " << t.get_id() << endl;
5. 支持 lambda(超方便!)
thread t([]{cout << "Lambda 线程运行!" << endl;this_thread::sleep_for(1s);
});
t.join();
6. 支持类成员函数
class Worker
{
public:void work(int n){cout << "Worker 工作 " << n << endl;}
};Worker w;
thread t(&Worker::work, &w, 100); // &w 是对象地址
t.join();
#include <thread>
#include <iostream>
using namespace std;// 普通函数
void func(string msg)
{cout << "主线程: " << msg << endl;
}// 成员函数
class work
{
public:void run(int x){cout << "子线程: " << x << endl;}
};int main()
{thread t1(func, "我是普通函数线程"); // 1. 普通函数线程work w;thread t2(&work::run, &w, 42); // 2. 成员函数线程thread t3([]() { // 3. lambda线程cout << "lambda线程: " << endl;});t1.join();t2.join();t3.join();return 0;
}
#include <iostream>
#include <thread>
#include <chrono> // C++ 中专门处理时间的库using namespace std;
using namespace chrono;void threadFunc(const string& name)
{for (int i = 0; i < 3; ++i){cout << name << ": 工作中 " << i << endl;this_thread::sleep_for(1s); // 睡1秒}
}int main()
{thread t(threadFunc, "子线程"); // 创建线程(pthread_create)for (int i = 0; i < 2; ++i) // 主线程也干点事{cout << "主线程: 主线程工作 " << i << endl;this_thread::sleep_for(500ms);}t.join(); // 等待子线程结束(类比 pthread_join)cout << "所有线程结束!" << endl;return 0;
}//运行结果示例:
//主线程:主线程工作 0
//子线程:工作中 0
//主线程:主线程工作 1
//子线程:工作中 1
//子线程:工作中 2
//所有线程结束!!
7. 可重入与线程安全
“可重入”指的是一个函数可以被多个线程同时调用,并且不会互相影响,不会出现混乱或崩溃。
1. 代码示例:一个不可重入的函数
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;int counter = 0; // 全局变量void* task(void* arg) // 所有线程都会调用这个函数
{// 每个线程都对同一个 counter 加 1for (int i = 0; i < 5; ++i){++counter;cout << "线程 " << pthread_self() << " counter = " << counter << endl;sleep(1);}pthread_exit(nullptr);
}int main()
{pthread_t t1, t2;pthread_create(&t1, nullptr, task, nullptr);pthread_create(&t2, nullptr, task, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);cout << "最终counter = " << counter << endl;return 0;
}
这个函数是 不是可重入的,并且运行输出是错乱的,因为 counter
是 全局变量,多个线程同时改它,结果错乱。cout
也是 共享资源,多个线程同时输出可能出现换行错乱。
2. 可重入函数应该是什么样?
// 完全不使用全局变量,只用局部变量
void* safe_task(void* arg)
{int local_counter = 0; // 每个线程有自己的变量for (int i = 0; i < 5; ++i){++local_counter;cout << "线程 " << pthread_self() << " local_counter = " << local_counter << endl;sleep(1);}pthread_exit(nullptr);
}
这个 safe_task()
就是 可重入的函数,每个线程都自己玩自己的变量,互不干扰。
3. 小结
现在用线程,只需要记住:可重入函数不使用全局变量,也不操作共享资源,就不会线程混乱。目前只需要做到:尽量只用局部变量,一个线程干自己的事,不要访问别人家的变量,就能避免 90% 的线程问题!
要注意的点 | 是否说明可重入 | 建议做法 |
---|---|---|
用全局变量 | ❌ 否 | 每个线程用自己的局部变量 |
打印输出 cout/printf | ❌ 否 | 少用或后续使用加锁保护输出 |
多个线程同时调函数 | ✅ 是 | 放心大胆用 |
pthread API | ✅ 无影响 | 这些 pthread 函数本身是线程安全的 |
8. 代码实战
1. 多种终止方式
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
using namespace std;void* thread1(void*)
{cout << "[线程1] 正常 return 退出" << endl; // 方法1:函数 return 自然返回return (void*)1; // 返回退出值 1// return nullptr;
}void* thread2(void*)
{cout << "[线程2] 使用 pthread_exit 主动退出" << endl; // 方法2:显式调用 pthread_exitpthread_exit((void*)2); // 返回退出值 2
}void* thread3(void*)
{cout << "[线程3] 进入无限循环,等待被取消..." << endl; // 方法3:无限循环,等待 cancelwhile (true){sleep(1); // 可取消点}return nullptr;
}void* thread4(void*) // 方法4:使用 exit 退出(注意:此会导致整个进程退出!不推荐!)
{cout << "[线程4] 调用了 exit(3),整个进程都会终止!" << endl;exit(3); // 会结束整个程序(不推荐在线程中用)
}int main()
{pthread_t t1, t2, t3, t4;void* ret;// 创建四个线程pthread_create(&t1, nullptr, thread1, nullptr);pthread_create(&t2, nullptr, thread2, nullptr);pthread_create(&t3, nullptr, thread3, nullptr);// pthread_create(&t4, nullptr, thread4, nullptr);// 等待线程1退出(正常 return)pthread_join(t1, &ret);cout << "[主线程] 线程1退出,退出值 = " << (long long)ret << "(return 返回)" << endl;// 等待线程2退出(pthread_exit)pthread_join(t2, &ret);cout << "[主线程] 线程2退出,退出值 = " << (long long)ret << "(pthread_exit 返回)" << endl;// 取消线程3sleep(2); // 等它跑一下pthread_cancel(t3);pthread_join(t3, &ret);if (ret == PTHREAD_CANCELED){cout << "[主线程] 线程3被取消(pthread_cancel)" << endl;}else{cout << "[主线程] 线程3退出值 = " << (long long)ret << endl;}sleep(2); // 等它跑一下// 等待线程4退出(exit)=> 会导致其他线程无法正常退出,整个进程终止!// pthread_join(t4, &ret);// cout << "[主线程] 线程4退出,退出值 = " << (long long)ret << "(exit 终止)" << endl;return 0;
}
2. 多线程的协同
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;// 枚举线程运行状态
enum Status
{OK = 0, // 正常ERROR // 异常
};class ThreadTask
{
public:// 构造函数:传入线程名字、起始数、结束数、线程编号ThreadTask(const string& name, int begin, int end, int id): _name(name), _begin(begin), _end(end), _id(id),_result(0), _status(Status::OK){}~ThreadTask() {} // 析构函数// 线程的实际任务函数void run(){for (int i = _begin; i <= _end; ++i){_result += i; // 计算指定区间的和}printf("线程[%s]运行完毕,计算[%d~%d]结束\n", _name.c_str(), _begin, _end);// pthread_exit:线程结束时返回一个指针(被主线程pthread_join获取)pthread_exit(this);}
public:string _name; // 线程名称int _begin; // 区间起点int _end; // 区间终点int _id; // 线程编号int _result; // 计算结果Status _status; // 线程状态
};// 线程入口函数(全局函数 / 静态函数)
void* threadEntry(void* arg)
{// static_cast<T*> 是 C++ 的类型转换运算符:用于在编译时进行类型安全的指针转换ThreadTask* task = static_cast<ThreadTask*>(arg);task->run(); // 执行实际任务(内部调用 pthread_exit 退出线程)return nullptr; // 实际不会执行到这里,因为 run() 中直接退出了线程
}int main()
{const int THREAD_NUM = 4; // 总线程数量pthread_t tids[THREAD_NUM]; // 线程ID数组ThreadTask* tasks[THREAD_NUM]; // 每个线程绑定一个任务对象int range = 100; // 每个线程负责计算100个数的和int start = 1; // 区间起始点for (int i = 0; i < THREAD_NUM; ++i) // 创建线程{string name = "Thread-" + to_string(i + 1);int end = start + range - 1;tasks[i] = new ThreadTask(name, start, end, i + 1); // 创建一个任务对象// 创建线程,执行 threadEntry 函数,参数传入任务对象指针pthread_create(&tids[i], nullptr, threadEntry, tasks[i]);start = end + 1;sleep(1); // 保证顺序输出(演示效果)}for (int i = 0; i < THREAD_NUM; ++i) // 等待线程结束 + 收集结果{void* ret = nullptr;pthread_join(tids[i], &ret); // pthread_join 等待线程退出,并获取其返回值ThreadTask* task = static_cast<ThreadTask*>(ret);if (task->_status == Status::OK){printf("%s计算[%d~%d]结果: %d\n", task->_name.c_str(), task->_begin, task->_end, task->_result);}else{printf("%s执行失败!\n", task->_name.c_str());}delete task; // 释放任务对象资源}cout << "所有线程协作完成!" << endl;return 0;
}
3. 多线程特性综合演示
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <string>
using namespace std;#define NUM 3// __thread 是 GCC 提供的线程局部存储关键字,用于声明在函数内的变量,每个线程都有自己的副本,互不干扰。
__thread unsigned int thread_local_number = 0; // 每个线程独有
__thread int thread_local_pid = 0; // 每个线程独有struct threadData // 线程数据结构
{string threadname; // 线程名称int thread_id; // 线程编号
};string toHex(pthread_t tid) // 将pthread_t转换为十六进制字符串(便于显示)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%lx", tid);// 使用%lx格式化pthread_treturn buffer;
}void InitThreadData(threadData* td, int number) // 初始化线程数据
{td->threadname = "thread-" + to_string(number);// 设置线程名称td->thread_id = number; // 设置线程ID
}void* threadRoutine(void* args) // 线程执行函数 - 所有线程都执行这个函数,但每个线程的数据是独立的
{pthread_detach(pthread_self()); // 立即将当前线程设置为分离状态,这样线程结束后会自动释放资源,无需主线程jointhreadData* td = static_cast<threadData*>(args);// 获取当前线程ID和进程IDstring tid_str = toHex(pthread_self()); // 当前线程的IDint process_pid = getpid(); // 进程ID(所有线程共享)// 初始化线程局部变量thread_local_number = td->thread_id * 100; // 每个线程有不同的值thread_local_pid = process_pid; // 每个线程都有自己的副本cout << "[" << td->threadname << "] 启动!"<< "线程ID: " << tid_str<< ", 进程ID: " << process_pid<< ", 线程局部变量 number: " << thread_local_number << endl;for (int i = 0; i < 5; i++) // 减少循环次数便于观察{// 展示线程的关键特性:// 1. 同一进程内的所有线程共享进程ID// 2. 每个线程有自己的线程ID// 3. 每个线程有自己的线程局部存储变量cout << "[" << td->threadname << "] " << "第" << (i + 1) << "次执行:"<< "线程ID: " << tid_str<< ", 进程ID: " << process_pid<< ", 局部number: " << thread_local_number<< ", 局部pid: " << thread_local_pid << endl;sleep(1); // 休眠1秒thread_local_number++; // 修改线程局部变量(只影响当前线程)}cout << "[" << td->threadname << "] 执行完毕!" << endl;delete td; // 清理分配的内存return nullptr; // 返回nullptr表示线程正常结束
}int main()
{cout << "主线程ID: " << toHex(pthread_self()) << endl;cout << "进程ID: " << getpid() << endl << endl;vector<pthread_t> tids; // 存储所有线程ID的容器cout << "开始创建 " << NUM << " 个线程..." << endl;for (int i = 0; i < NUM; i++) // 创建多个线程{pthread_t tid; // 线程ID变量threadData* td = new threadData; // 为每个线程分配独立的数据InitThreadData(td, i); // 初始化线程数据int ret = pthread_create(&tid, nullptr, threadRoutine, td);if (ret != 0){cerr << "创建线程失败: " << strerror(ret) << endl;delete td; // 创建失败时释放内存continue;}cout << "成功创建线程 " << td->threadname << " (ID: " << toHex(tid) << ")" << endl;tids.push_back(tid); // 保存线程IDusleep(100000); // 延时0.1秒,确保线程正确启动}cout << "\n所有线程创建完成,主线程等待..." << endl;sleep(2); // 给线程一些执行时间cout << "\n=== 尝试对分离线程进行join操作 ===" << endl; // 尝试对分离线程进行join操作(这会失败)for (size_t i = 0; i < tids.size(); i++){int result = pthread_join(tids[i], nullptr);printf("对线程 0x%lx 执行join的结果: %d (%s)\n", tids[i], result, strerror(result));// 由于线程已被分离,这里会返回EINVAL(无效参数)}cout << "1. 线程共享进程资源(如进程ID、文件描述符等)" << endl;cout << "2. 每个线程有自己的线程ID和栈空间" << endl;cout << "3. __thread关键字创建线程局部存储变量" << endl;cout << "4. 分离线程结束后自动释放资源" << endl;cout << "5. 分离线程不能被pthread_join回收" << endl;cout << "6. 线程执行是并发的,输出顺序不同" << endl;cout << "\n主线程继续执行中..." << endl;sleep(5);cout << "主线程执行完毕,程序退出!" << endl;return 0;
}
共勉