线程控制
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.cc g++ -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(); // 获取当前线程 ID
cout << "子线程 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(); // 主线程自身 ID
cout << "主线程 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); // 所有线程都要分配内存,让他们分别乘以 10
cout << "子线程 " << 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); // 主动退出,返回值是1234
cout << "子线程退出已经退出了,并且这一句话不会被执行!" << 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 加 1
for (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_exit
pthread_exit((void*)2); // 返回退出值 2
}
void* thread3(void*)
{
cout << "[线程3] 进入无限循环,等待被取消..." << endl; // 方法3:无限循环,等待 cancel
while (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;
// 取消线程3
sleep(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_t
return 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()); // 立即将当前线程设置为分离状态,这样线程结束后会自动释放资源,无需主线程join
threadData* td = static_cast<threadData*>(args);
// 获取当前线程ID和进程ID
string tid_str = toHex(pthread_self()); // 当前线程的ID
int 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); // 保存线程ID
usleep(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;
}