C语言与C++语言多线程编程完整教程
C语言与C++语言多线程编程完整教程
目录
-
多线程基础理论
-
C语言POSIX线程(pthread)
-
C++标准线程库详解
-
线程同步机制深度解析
-
线程间通信与数据共享
-
高级线程技术
-
常见问题与性能优化
-
最佳实践与设计模式
1. 多线程基础理论
1.1 进程与线程的核心概念
1.1.1 什么是进程
进程(Process)是操作系统进行资源分配的基本单位。每一个运行中的程序都是一个进程,拥有独立的地址空间、内存、文件描述符、打开的文件、信号处理器等资源。
-
进程拥有独立的虚拟地址空间,一个进程无法直接访问另一个进程的内存
-
进程间通信(IPC)需要通过专门的机制:管道、消息队列、共享内存、套接字等
-
进程切换开销大,需要保存整个进程上下文并切换页表
1.1.2 什么是线程
线程(Thread)是操作系统进行CPU调度的最小单位,也被称为轻量级进程。线程存在于进程之中,共享进程的资源。
-
一个进程可以包含一个或多个线程,所有线程共享同一块地址空间
-
每个线程拥有独立的:程序计数器、栈空间、寄存器集合、线程局部存储
-
线程间可以直接通过共享内存通信,开销远小于进程间通信
-
线程切换只需保存和恢复少量寄存器,开销远小于进程切换
1.1.3 进程与线程的对比
| 特性 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 独立拥有全部资源 | 共享进程资源 |
| 地址空间 | 独立地址空间 | 共享地址空间 |
| 通信开销 | 大(需IPC机制) | 小(直接共享内存) |
| 切换开销 | 大(页表切换等) | 小(仅寄存器切换) |
| 稳定性 | 高,进程间互不影响 | 低,一个线程崩溃影响整个进程 |
| 创建销毁 | 慢 | 快 |
1.2 并发与并行
1.2.1 并发(Concurrency)
并发是指在宏观上多个任务同时进行,微观上这些任务可能是交替执行的。在单核CPU上,操作系统通过时间片轮转让多个线程轮流执行,从用户视角看像是同时在运行。
-
核心是快速切换,造成"同时运行"的假象
-
单核CPU只能实现并发,无法实现真正的并行
-
适用于IO密集型任务,等待IO时CPU可以切换到其他线程
1.2.2 并行(Parallelism)
并行是指在物理层面多个任务真的同时执行,需要多核CPU的支持。每个核心可以独立执行一个线程。
-
真正的同时执行,不需要交替
-
必须依赖多核硬件
-
适用于计算密集型任务,可以显著提升执行速度
1.3 为什么使用多线程
-
充分利用多核CPU:现代CPU都是多核架构,单线程只能利用一个核心,多线程可以将计算任务分发到多个核心并行执行
-
提升响应速度:在GUI程序中,将耗时操作放在后台线程,保持界面响应
-
改善IO效率:当线程等待网络或磁盘IO时,CPU可以切换到其他线程继续工作
-
简化程序结构:某些问题天然适合分解为多个独立执行的任务
-
资源共享:线程间共享内存,比进程间通信更高效
1.4 多线程带来的挑战
-
竞态条件(Race Condition):多个线程同时读写共享数据,结果依赖于线程执行的时序
-
死锁(Deadlock):多个线程互相等待对方持有的资源,导致永久阻塞
-
数据竞争(Data Race):未同步的并发访问导致未定义行为
-
调试困难:多线程Bug具有随机性,难以复现和定位
-
上下文切换开销:线程过多会导致频繁切换,反而降低性能
2. C语言POSIX线程(pthread)
POSIX线程(Portable Operating System Interface Threads,简称pthread)是Unix/Linux系统下的标准线程API,定义在 <pthread.h> 头文件中。编译时必须加上 -pthread 选项链接线程库。
2.1 线程的创建与等待
2.1.1 核心概念
每个线程都有一个唯一的标识符 pthread_t,本质是一个无符号长整型。线程从指定的入口函数开始执行,函数签名必须是 void* (*start_routine)(void*),即返回值和参数都是 void* 类型。
线程创建后有两种状态:
-
可连接状态(Joinable):默认状态。线程结束后不会自动释放资源,必须由其他线程调用
pthread_join来回收资源并获取返回值 -
分离状态(Detached):线程结束后系统自动回收资源,不能被其他线程等待
2.1.2 主要API说明
pthread_create - 创建线程
int pthread_create(pthread_t *thread, // 输出参数,返回新线程的ID
const pthread_attr_t *attr, // 线程属性,NULL为默认属性
void *(*start_routine)(void*), // 线程入口函数指针
void *arg); // 传递给线程函数的参数
-
返回值:成功返回0,失败返回错误码
-
线程创建后立即开始执行,不需要额外的启动函数
pthread_join - 等待线程结束
int pthread_join(pthread_t thread, // 要等待的线程ID
void **retval); // 输出参数,接收线程的返回值
-
阻塞调用,直到指定线程结束才返回
-
只能对可连接状态的线程调用
-
一个线程只能被join一次
pthread_exit - 退出当前线程
void pthread_exit(void *retval);
-
在线程函数内部任意位置调用,立即终止当前线程
-
参数作为线程返回值,可被pthread_join获取
-
与return的区别:return只是从函数返回,只有最外层函数return才会终止线程;pthread_exit在任何层级调用都会终止线程
2.1.3 完整示例:创建并等待单个线程
#include <stdio.h> // 引入标准输入输出库,提供printf等函数
#include <stdlib.h> // 引入标准库,提供exit、EXIT_FAILURE等宏
#include <pthread.h> // 引入POSIX线程库,提供所有线程相关API
// 线程入口函数,pthread规定必须返回void*,参数为void*
// arg参数用于接收主线程传递过来的数据
void* thread_work(void* arg) {
// 将void*类型的参数转换为int*指针,再解引用得到实际的整数值
// 这是pthread传参的标准方式:任何类型都先转成void*传递
int thread_num = *(int*)arg;
// 打印线程信息,%d用于格式化输出整数
printf("【子线程%d】我开始工作了\n", thread_num);
// 模拟业务逻辑,这里简单做一个循环计算
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i; // 累加计算
}
printf("【子线程%d】计算完成,1到100的和是%d\n", thread_num, sum);
// 线程结束,返回计算结果
// 注意:不能返回局部变量的地址,因为函数返回后局部变量会销毁
// 这里将整数值直接强转为void*返回(仅限小整数场景演示)
return (void*)(long)sum;
}
// 主函数,程序入口,主线程从这里开始执行
int main() {
pthread_t tid; // 定义线程ID变量,用于标识新创建的线程
int arg_value = 1; // 准备传递给子线程的参数
int ret; // 存储函数返回值,用于错误检查
printf("【主线程】程序启动,准备创建子线程\n");
// 创建一个新线程
// 第1个参数:&tid,将新线程的ID写入tid变量
// 第2个参数:NULL,使用默认线程属性(可连接、默认栈大小等)
// 第3个参数:thread_work,线程开始执行的函数地址
// 第4个参数:&arg_value,传递给线程函数的参数指针
ret = pthread_create(&tid, NULL, thread_work, &arg_value);
// 检查线程创建是否成功
// pthread系列函数成功返回0,失败返回非0错误码
if (ret != 0) {
// perror会自动根据错误码打印对应的错误描述
perror("线程创建失败");
// 异常退出程序,EXIT_FAILURE表示失败状态
exit(EXIT_FAILURE);
}
printf("【主线程】子线程已创建,线程ID为%lu\n", (unsigned long)tid);
void* result; // 定义指针变量,用于接收子线程的返回值
printf("【主线程】等待子线程执行完毕...\n");
// 阻塞等待指定线程结束
// 第1个参数:要等待的线程ID
// 第2个参数:&result,线程的返回值会存入result指向的位置
// 如果不需要返回值,第二个参数可以传NULL
ret = pthread_join(tid, &result);
if (ret != 0) {
perror("等待线程失败");
exit(EXIT_FAILURE);
}
// 将返回值转换回long类型并打印
printf("【主线程】子线程已结束,返回结果是%ld\n", (long)result);
printf("【主线程】程序正常结束\n");
return 0; // 主线程正常返回,整个进程结束
}
编译运行命令:
gcc -o thread_demo thread_demo.c -pthread
./thread_demo
2.2 多线程批量创建与管理
2.2.1 核心知识点
实际开发中通常需要创建多个工作线程。需要注意一个常见陷阱:如果循环创建线程时传递循环变量的地址,所有线程会共享同一个变量地址,导致线程读取到错误的参数值。
正确做法有两种:
-
将值直接强转为
void*传递(适合小整数) -
为每个线程单独分配内存空间存储参数
2.2.2 完整示例:创建多个线程
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_COUNT 5 // 定义要创建的线程总数
// 线程执行函数
void* worker(void* arg) {
// 接收参数:直接将void*转换为long类型的数值
// 这种方式不需要额外内存分配,但只适合能塞进指针大小的数据
long thread_id = (long)arg;
printf("线程%ld:启动成功,开始执行任务\n", thread_id);
// 模拟不同线程的工作负载
// 每个线程计算自己ID范围内的累加和
long start = thread_id * 100 + 1;
long end = (thread_id + 1) * 100;
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
printf("线程%ld:计算完成,范围[%ld, %ld],和为%ld\n", thread_id, start, end, sum);
// 返回计算结果
pthread_exit((void*)sum);
}
int main() {
pthread_t threads[THREAD_COUNT]; // 线程ID数组,保存所有线程标识
int rc; // 存储函数返回码
printf("主线程:开始创建%d个工作线程\n", THREAD_COUNT);
// 循环创建多个线程
for (long i = 0; i < THREAD_COUNT; i++) {
printf("主线程:正在创建第%ld个线程\n", i);
// 创建线程,注意传参方式
// 将i的值直接强转为void*,每个线程拿到的是独立的值
// 错误写法:&i —— 所有线程共享同一个i的地址,i在循环中不断变化
rc = pthread_create(&threads[i], NULL, worker, (void*)i);
if (rc != 0) {
printf("错误:创建线程%ld失败,错误码%d\n", i, rc);
exit(EXIT_FAILURE);
}
}
printf("主线程:所有线程创建完毕,等待它们执行完成\n");
// 循环等待所有线程结束
for (long i = 0; i < THREAD_COUNT; i++) {
void* status; // 存储每个线程的返回值
// 等待第i个线程结束
rc = pthread_join(threads[i], &status);
if (rc != 0) {
printf("错误:等待线程%ld失败\n", i);
continue;
}
printf("主线程:线程%ld已退出,返回值%ld\n", i, (long)status);
}
printf("主线程:所有线程执行完毕,程序结束\n");
return 0;
}
2.3 线程分离状态
2.3.1 核心知识点
默认创建的线程是可连接的,必须调用 pthread_join 回收资源。如果忘记join,线程结束后会残留资源,造成类似"僵尸进程"的资源泄漏。
分离状态的线程结束后,操作系统会自动回收其所有资源,无需也不能被其他线程join。适合"发射后不管"的后台任务。
设置分离状态有两种方式:
-
创建后调用
pthread_detach() -
创建时通过属性设置为分离状态
2.3.2 完整示例:线程分离
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> // 提供sleep函数
// 后台线程函数,模拟一个周期性任务
void* background_task(void* arg) {
int task_id = *(int*)arg;
printf("后台任务%d:启动,开始周期性工作\n", task_id);
// 模拟后台任务循环执行3次
for (int i = 1; i <= 3; i++) {
sleep(1); // 休眠1秒,模拟工作耗时
printf("后台任务%d:第%d次工作完成\n", task_id, i);
}
printf("后台任务%d:全部工作完成,自动退出并释放资源\n", task_id);
// 分离线程的返回值没人能接收,返回NULL即可
return NULL;
}
int main() {
pthread_t tid;
int task_num = 101;
printf("主线程:创建后台线程\n");
// 创建线程
pthread_create(&tid, NULL, background_task, &task_num);
// 将线程设置为分离状态
// 一旦设置为分离,就不能再转回可连接状态
// 分离后的线程结束时系统自动回收资源
pthread_detach(tid);
printf("主线程:线程已分离,我继续做自己的事\n");
// 主线程继续执行其他逻辑
for (int i = 1; i <= 5; i++) {
sleep(1);
printf("主线程:正在处理第%d项工作\n", i);
}
printf("主线程:所有工作完成,准备退出\n");
printf("主线程:注意:如果我先退出,整个进程结束,分离线程也会被强制终止\n");
// 主线程退出,整个进程结束
// 分离线程如果还没执行完也会跟着结束
return 0;
}
2.4 线程属性详解
2.4.1 核心知识点
pthread_attr_t 是线程属性对象,可以精细控制线程的各项特性。使用流程是:初始化属性对象 → 设置各项属性 → 创建线程 → 销毁属性对象。
常用属性包括:
-
分离状态:设置线程创建即分离
-
栈大小:自定义线程栈空间大小
-
栈地址:手动指定栈内存位置
-
调度策略:设置线程调度优先级策略
-
继承调度器:是否继承创建者的调度属性
2.4.2 完整示例:自定义线程属性
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <limits.h> // PTHREAD_STACK_MIN
void* thread_func(void* arg) {
printf("子线程:我运行在自定义属性的线程中\n");
// 获取当前线程的栈大小信息(仅作演示)
pthread_attr_t attr;
size_t stack_size;
// 获取当前线程的属性
pthread_getattr_np(pthread_self(), &attr);
pthread_attr_getstacksize(&attr, &stack_size);
printf("子线程:我的栈大小是%zu字节(约%.2f MB)\n",
stack_size, (double)stack_size / 1024 / 1024);
pthread_attr_destroy(&attr);
return NULL;
}
int main() {
pthread_t tid;
pthread_attr_t attr; // 定义线程属性对象
int ret;
// 第一步:初始化属性对象
// 必须先初始化才能设置各种属性
ret = pthread_attr_init(&attr);
if (ret != 0) {
perror("属性初始化失败");
exit(EXIT_FAILURE);
}
// 设置属性1:创建即分离状态
// PTHREAD_CREATE_JOINABLE - 可连接(默认)
// PTHREAD_CREATE_DETACHED - 分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
printf("主线程:已设置线程为分离状态\n");
// 设置属性2:自定义栈大小
// 栈不能小于系统最小值PTHREAD_STACK_MIN
// 这里设置为2MB
size_t stack_size = 2 * 1024 * 1024; // 2MB
if (stack_size < PTHREAD_STACK_MIN) {
stack_size = PTHREAD_STACK_MIN;
}
pthread_attr_setstacksize(&attr, stack_size);
printf("主线程:已设置栈大小为2MB\n");
// 设置属性3:设置调度策略为SCHED_OTHER(默认分时调度)
// 还有SCHED_FIFO、SCHED_RR等实时调度策略(需要root权限)
pthread_attr_setschedpolicy(&attr, SCHED_OTHER);
printf("主线程:已设置调度策略为默认分时调度\n");
// 使用配置好的属性创建线程
ret = pthread_create(&tid, &attr, thread_func, NULL);
if (ret != 0) {
perror("线程创建失败");
exit(EXIT_FAILURE);
}
// 线程创建完成后,属性对象就可以销毁了
// 销毁属性对象不影响已经创建的线程
pthread_attr_destroy(&attr);
printf("主线程:属性对象已销毁\n");
// 因为线程是分离的,不能join,稍等片刻让它执行
sleep(1);
printf("主线程:程序结束\n");
return 0;
}
3. C++标准线程库详解
C++11标准首次将多线程纳入标准库,提供了 std::thread 等一系列跨平台线程工具。相比pthread的C风格API,C++线程库更加类型安全、易用,并且支持函数对象、Lambda、成员函数等多种线程入口形式。
3.1 std::thread 基础
3.1.1 核心概念
std::thread 是线程的包装类,一个 std::thread 对象代表一个执行线程。
关键特性:
-
线程对象构造后立即开始执行,没有单独的start方法
-
每个
std::thread对象最多关联一个执行线程 -
线程对象可以处于"可连接"或"不可连接"状态
-
可连接的线程对象销毁前必须调用
join()或detach(),否则会调用std::terminate终止程序 -
线程对象支持移动语义,不支持拷贝语义
3.1.2 线程函数的多种形式
std::thread 的构造函数非常灵活,可以接受各种可调用对象:
-
普通函数指针
-
函数对象(仿函数)
-
Lambda表达式
-
类的非静态成员函数(需要对象指针)
-
类的静态成员函数
3.1.3 完整示例:多种方式创建线程
#include <iostream> // C++标准输入输出流,提供cout
#include <thread> // C++线程库头文件,提供std::thread
#include <string> // 字符串类
// ==============================================
// 方式1:普通函数作为线程入口
// ==============================================
void normal_func(int a, const std::string& msg) {
// std::cout用于输出,std::endl输出换行并刷新缓冲区
std::cout << "普通函数线程: a = " << a << ", msg = " << msg << std::endl;
}
// ==============================================
// 方式2:函数对象(仿函数)类
// ==============================================
class Functor {
public:
// 重载()运算符,使对象可以像函数一样调用
void operator()(int x) const {
std::cout << "仿函数线程: x = " << x << std::endl;
}
};
// ==============================================
// 方式3:演示用的类,包含成员函数
// ==============================================
class MyClass {
public:
// 非静态成员函数,隐含this指针
void member_func(int n) {
std::cout << "成员函数线程: n = " << n << ", 本对象地址: " << this << std::endl;
}
// 静态成员函数,不需要this指针
static void static_func(int n) {
std::cout << "静态成员函数线程: n = " << n << std::endl;
}
};
int main() {
std::cout << "===== 主线程开始 =====" << std::endl;
// ------------------------------------------
// 1. 使用普通函数创建线程
// std::thread构造函数第一个参数是函数指针,后面是函数参数
// 参数会被复制到线程内部存储
// ------------------------------------------
std::thread t1(normal_func, 10, "Hello World");
t1.join(); // 等待t1执行完毕,阻塞当前线程
// ------------------------------------------
// 2. 使用仿函数创建线程
// 传入Functor类的临时对象作为线程函数
// ------------------------------------------
Functor func;
std::thread t2(func, 20);
t2.join();
// ------------------------------------------
// 3. 使用Lambda表达式创建线程(最常用)
// []是捕获列表,指定可以访问的外部变量
// [=]按值捕获所有变量,[&]按引用捕获所有变量
// ------------------------------------------
int local_var = 30;
std::thread t3([local_var]() {
// local_var按值捕获,在线程内部是一份独立的拷贝
std::cout << "Lambda线程: local_var = " << local_var << std::endl;
});
t3.join();
// ------------------------------------------
// 4. 使用类的非静态成员函数
// 需要两个参数:成员函数指针 + 对象实例的地址
// &MyClass::member_func 是成员函数指针语法
// ------------------------------------------
MyClass obj;
std::thread t4(&MyClass::member_func, &obj, 40);
t4.join();
// ------------------------------------------
// 5. 使用类的静态成员函数
// 不需要对象指针,直接传静态函数指针
// ------------------------------------------
std::thread t5(&MyClass::static_func, 50);
t5.join();
std::cout << "===== 所有线程执行完毕 =====" << std::endl;
return 0;
}
编译命令:
g++ -std=c++11 -o thread_basic thread_basic.cpp -pthread
3.2 join 与 detach 深入理解
3.2.1 核心知识点
join(连接):
-
阻塞当前线程,直到目标线程执行完毕
-
调用join后,线程对象变为不可连接状态
-
用于需要等待线程结果的场景
-
join会清理线程的所有资源
detach(分离):
-
将线程与线程对象分离,线程在后台独立运行
-
调用detach后,线程对象变为不可连接状态
-
线程结束后自动释放资源
-
适用于不需要等待结果的后台任务
joinable():判断线程对象是否可连接(即是否关联着一个活跃线程)
重要原则:
-
一个线程只能join或detach一次
-
线程对象销毁前必须处于不可连接状态,否则程序崩溃
-
detach后无法再与线程交互,也无法再join
3.2.2 完整示例
#include <iostream>
#include <thread>
#include <chrono> // 时间库,提供时长类
// 线程工作函数,模拟耗时操作
void worker(const std::string& name) {
for (int i = 0; i < 3; i++) {
// std::this_thread是当前线程相关的操作命名空间
// sleep_for让当前线程休眠指定时长
// std::chrono::milliseconds表示毫秒时长
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << name << ": 第" << i + 1 << "次工作" << std::endl;
}
std::cout << name << ": 全部工作完成" << std::endl;
}
int main() {
std::cout << "=== join示例 ===" << std::endl;
// 创建线程t1
std::thread t1(worker, "线程A");
// joinable()检查线程是否可连接
// 刚创建且未join/detach的线程是可连接的
std::cout << "t1创建后,joinable = " << std::boolalpha << t1.joinable() << std::endl;
if (t1.joinable()) {
t1.join(); // 阻塞等待线程结束
std::cout << "t1 join后,joinable = " << t1.joinable() << std::endl;
}
// 不能重复join
// t1.join(); // 这行会导致未定义行为
std::cout << "\n=== detach示例 ===" << std::endl;
// 创建线程t2
std::thread t2(worker, "线程B");
std::cout << "t2创建后,joinable = " << t2.joinable() << std::endl;
// detach分离线程,线程后台运行
t2.detach();
std::cout << "t2 detach后,joinable = " << t2.joinable() << std::endl;
std::cout << "主线程:t2已分离,我不等它了" << std::endl;
// 主线程稍等一下,让detach的线程有机会输出
// 如果主线程先退出,整个进程结束,detach线程也会被终止
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "\n主线程:程序结束" << std::endl;
return 0;
}
3.3 线程标识与硬件信息
3.3.1 核心知识点
线程ID(std::thread::id):
-
每个线程有唯一的ID标识
-
通过
std::this_thread::get_id()获取当前线程ID -
通过线程对象的
get_id()成员函数获取指定线程ID -
可以比较大小、输出、作为容器键值
硬件并发数:
-
std::thread::hardware_concurrency()返回CPU支持的并发线程数 -
通常等于CPU逻辑核心数
-
如果无法获取则返回0
-
创建计算密集型线程时,数量一般不超过这个值
3.3.2 完整示例
#include <iostream>
#include <thread>
int main() {
// 获取当前线程(主线程)的ID
std::thread::id main_tid = std::this_thread::get_id();
std::cout << "主线程ID: " << main_tid << std::endl;
// 获取硬件支持的并发线程数
// 这是一个静态成员函数,不需要线程对象
unsigned int core_count = std::thread::hardware_concurrency();
if (core_count == 0) {
std::cout << "无法获取硬件并发数" << std::endl;
} else {
std::cout << "CPU逻辑核心数: " << core_count << std::endl;
std::cout << "建议计算密集型线程数不超过此值" << std::endl;
}
// 创建一个子线程,在子线程中获取自己的ID
std::thread t([]() {
// std::this_thread::get_id() 获取当前执行线程的ID
std::thread::id tid = std::this_thread::get_id();
std::cout << "子线程ID: " << tid << std::endl;
});
// 从外部获取子线程的ID
std::cout << "从外部获取子线程ID: " << t.get_id() << std::endl;
t.join();
// join后线程对象不再关联线程,get_id返回默认值
std::cout << "join后t.get_id(): " << t.get_id() << std::endl;
// 两个默认id相等
std::thread::id default_id;
std::cout << "默认id等于join后的id: "
<< std::boolalpha << (t.get_id() == default_id) << std::endl;
return 0;
}
3.4 线程参数传递的细节
3.4.1 核心知识点
std::thread 构造函数的参数默认是按值拷贝到线程内部的。这一点非常重要,很多bug都源于对传参方式的误解。
传参规则:
-
基本类型默认按值拷贝
-
对象类型默认调用拷贝构造函数复制一份
-
如果确实需要传递引用,必须使用
std::ref()或std::cref()包装 -
指针按值拷贝指针本身(指向的内容还是同一份)
-
移动语义:使用
std::move()转移对象所有权
3.4.2 完整示例:参数传递详解
#include <iostream>
#include <thread>
#include <string>
// 接受引用参数的函数
void modify_value(int& x) {
x = 100; // 修改引用指向的值
std::cout << "线程内部: x = " << x << " (地址: " << &x << ")" << std::endl;
}
// 接受const引用的函数
void print_string(const std::string& str) {
std::cout << "字符串内容: " << str << " (地址: " << &str << ")" << std::endl;
}
// 接受右值引用的函数(移动语义)
void take_ownership(std::string&& str) {
std::string local = std::move(str); // 移动构造
std::cout << "移动后字符串: " << local << std::endl;
}
int main() {
std::cout << "=== 测试1:默认按值拷贝 ===" << std::endl;
int a = 10;
std::cout << "修改前: a = " << a << " (地址: " << &a << ")" << std::endl;
// 注意:直接传a的话,thread会拷贝一份
// 即使函数参数是int&,也会绑定到拷贝的副本上
// 所以下面这行编译会报错,因为非常量引用不能绑定到临时对象
// std::thread t1(modify_value, a); // 编译错误!
// 必须用std::ref显式包装才能传递真正的引用
std::thread t1(modify_value, std::ref(a));
t1.join();
std::cout << "修改后: a = " << a << " (地址: " << &a << ")" << std::endl;
std::cout << "使用std::ref后,线程确实修改了外部变量" << std::endl;
std::cout << "\n=== 测试2:const引用 ===" << std::endl;
std::string s = "Hello C++ Thread";
std::cout << "外部字符串地址: " << &s << std::endl;
// 即使函数参数是const&,默认也是先拷贝再传引用
// 线程内部的str是拷贝的副本
std::thread t2(print_string, s);
t2.join();
std::cout << "可以看到地址不同,说明发生了拷贝" << std::endl;
// 如果想避免拷贝,用std::cref(const reference)
std::thread t3(print_string, std::cref(s));
t3.join();
std::cout << "使用std::cref后,地址相同,没有拷贝" << std::endl;
std::cout << "\n=== 测试3:移动语义 ===" << std::endl;
std::string big_data = "这是一大段数据";
std::cout << "移动前: big_data = \"" << big_data << "\"" << std::endl;
// 使用std::move将对象移动到线程中
// 移动后原对象处于有效但未指定的状态(通常为空)
std::thread t4(take_ownership, std::move(big_data));
t4.join();
std::cout << "移动后: big_data = \"" << big_data << "\"" << std::endl;
return 0;
}
4. 线程同步机制深度解析
4.1 竞态条件与临界区
4.1.1 核心概念
竞态条件(Race Condition):
多个线程同时访问共享数据,且至少有一个是写操作,最终结果依赖于线程执行的相对时序,就产生了竞态条件。
为什么简单的 i++ 不是原子操作?
-
第一步:从内存读取i的值到寄存器
-
第二步:寄存器中加1
-
第三步:将结果写回内存
这三步之间都可能被打断,导致另一个线程读取到旧值,最终造成数据丢失。
临界区(Critical Section):
访问共享资源的代码片段,同一时间只能有一个线程执行。必须使用同步机制保护临界区。
4.1.2 竞态条件演示示例
#include <stdio.h>
#include <pthread.h>
#define TIMES 100000 // 每个线程累加次数
int shared_counter = 0; // 共享全局变量,没有任何保护
// 线程函数:对共享变量累加
void* increment(void* arg) {
for (int i = 0; i < TIMES; i++) {
// 这行代码看似简单,实际包含读-改-写三步
// 多线程并发执行时会发生竞态条件
shared_counter++;
}
return NULL;
}
int main() {
pthread_t t1, t2;
// 创建两个线程,同时对同一个变量累加
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
// 等待两个线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 预期结果:2 * TIMES = 200000
// 实际结果:几乎总是小于200000,每次运行结果都可能不同
printf("最终结果: %d\n", shared_counter);
printf("预期结果: %d\n", 2 * TIMES);
printf("差值: %d\n", 2 * TIMES - shared_counter);
printf("这就是竞态条件导致的数据丢失\n");
return 0;
}
4.2 互斥锁(Mutex)
4.2.1 核心原理
互斥锁(Mutual Exclusion,简称Mutex)是最基本的同步原语。它就像一把锁,进入临界区前加锁,离开临界区后解锁。同一时间只有一个线程能持有锁,其他线程尝试加锁时会被阻塞。
互斥锁保证了三点:
-
原子性:临界区内的操作看起来是不可分割的
-
互斥性:同一时间只有一个线程在临界区内
-
有序性:临界区前的操作对后续进入的线程可见
4.2.2 C语言 pthread_mutex 详解
主要API:
-
pthread_mutex_init:初始化互斥锁 -
pthread_mutex_destroy:销毁互斥锁 -
pthread_mutex_lock:加锁,阻塞等待 -
pthread_mutex_trylock:尝试加锁,不阻塞,失败立即返回 -
pthread_mutex_unlock:解锁
完整示例:使用互斥锁保护共享变量
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#define TIMES 100000
#define THREAD_NUM 4
int shared_counter = 0; // 共享计数器
pthread_mutex_t counter_mutex; // 保护计数器的互斥锁
// 线程工作函数
void* worker(void* arg) {
long thread_id = (long)arg;
for (int i = 0; i < TIMES; i++) {
// ===== 加锁 =====
// 获取互斥锁,如果锁已被其他线程持有,则阻塞等待
// 这是阻塞式加锁,直到拿到锁才返回
pthread_mutex_lock(&counter_mutex);
// ===== 临界区开始 =====
// 这里的代码同一时间只有一个线程能执行
// 对共享变量的操作必须全部放在临界区内
shared_counter++;
// ===== 临界区结束 =====
// ===== 解锁 =====
// 释放互斥锁,其他等待的线程可以获取锁
// 必须确保所有退出路径都能解锁,否则会死锁
pthread_mutex_unlock(&counter_mutex);
}
printf("线程%ld:完成%d次累加\n", thread_id, TIMES);
return NULL;
}
int main() {
pthread_t threads[THREAD_NUM];
int ret;
// 初始化互斥锁
// 参数1:互斥锁指针
// 参数2:锁属性,NULL使用默认属性(快速互斥锁)
ret = pthread_mutex_init(&counter_mutex, NULL);
if (ret != 0) {
perror("互斥锁初始化失败");
exit(EXIT_FAILURE);
}
printf("主线程:创建%d个线程,每个累加%d次\n", THREAD_NUM, TIMES);
// 创建多个线程
for (long i = 0; i < THREAD_NUM; i++) {
pthread_create(&threads[i], NULL, worker, (void*)i);
}
// 等待所有线程结束
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(threads[i], NULL);
}
// 打印结果
printf("\n最终结果: %d\n", shared_counter);
printf("预期结果: %d\n", THREAD_NUM * TIMES);
if (shared_counter == THREAD_NUM * TIMES) {
printf("✓ 结果正确,互斥锁有效防止了竞态条件\n");
} else {
printf("✗ 结果错误\n");
}
// 销毁互斥锁,释放资源
// 必须确保没有线程持有锁时才能销毁
pthread_mutex_destroy(&counter_mutex);
return 0;
}
4.2.3 C++ std::mutex 与 RAII锁包装器
C++标准库提供了多种互斥锁类型和锁包装器,利用RAII(资源获取即初始化)思想自动管理锁的生命周期,避免忘记解锁。
互斥锁类型:
-
std::mutex:最基础的互斥锁 -
std::recursive_mutex:递归互斥锁,同一线程可多次加锁 -
std::timed_mutex:带超时的互斥锁 -
std::recursive_timed_mutex:递归+超时
锁包装器:
-
std::lock_guard:简单的RAII包装,构造加锁,析构解锁 -
std::unique_lock:功能更强大的锁包装,支持手动加解锁、延迟加锁、超时、移动语义
完整示例:C++互斥锁的各种用法
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <string>
// 共享计数器
int counter = 0;
// 标准互斥锁
std::mutex mtx;
// 递归互斥锁(演示用)
std::recursive_mutex rec_mtx;
// ==========================================
// 方式1:手动lock/unlock(不推荐,容易忘记解锁)
// ==========================================
void manual_lock_increment(int times) {
for (int i = 0; i < times; i++) {
mtx.lock(); // 手动加锁
counter++; // 临界区操作
mtx.unlock(); // 手动解锁
// 问题:如果中间抛出异常,unlock不会执行,造成死锁
}
}
// ==========================================
// 方式2:std::lock_guard(推荐,简单场景)
// ==========================================
void lock_guard_increment(int times) {
for (int i = 0; i < times; i++) {
// lock_guard在构造时自动加锁
// 析构时自动解锁(无论正常返回还是异常抛出)
// 这是RAII思想的典型应用
std::lock_guard<std::mutex> guard(mtx);
counter++; // 临界区
} // 这里guard析构,自动调用mtx.unlock()
}
// ==========================================
// 方式3:std::unique_lock(灵活场景)
// ==========================================
void unique_lock_demo() {
// defer_lock表示构造时不加锁,稍后手动加
std::unique_lock<std::mutex> ulock(mtx, std::defer_lock);
// ... 可以做一些不需要锁的准备工作 ...
ulock.lock(); // 手动加锁
counter++; // 临界区操作
ulock.unlock(); // 可以提前解锁,减少锁持有时间
// ... 做一些不需要锁的计算 ...
// 还可以再次加锁
ulock.lock();
counter++;
ulock.unlock();
// 析构时如果还持有锁会自动解锁
}
// ==========================================
// 递归锁演示
// ==========================================
void recursive_func(int depth) {
if (depth <= 0) return;
// 递归锁允许同一线程多次加锁
// 如果用普通mutex,同一线程重复加锁会导致死锁
std::lock_guard<std::recursive_mutex> guard(rec_mtx);
std::cout << "递归深度: " << depth << std::endl;
recursive_func(depth - 1); // 递归调用,再次加锁
// 每层析构解锁一次,加锁次数等于解锁次数
}
int main() {
const int thread_num = 5;
const int per_thread = 10000;
std::cout << "=== 测试lock_guard保护的并发累加 ===" << std::endl;
std::vector<std::thread> threads;
// 创建多个线程
for (int i = 0; i < thread_num; i++) {
threads.emplace_back(lock_guard_increment, per_thread);
}
// 等待所有线程
for (auto& t : threads) {
t.join();
}
std::cout << "最终结果: " << counter << std::endl;
std::cout << "预期结果: " << thread_num * per_thread << std::endl;
// 重置计数器
counter = 0;
threads.clear();
std::cout << "\n=== 测试unique_lock ===" << std::endl;
std::thread t1(unique_lock_demo);
std::thread t2(unique_lock_demo);
t1.join();
t2.join();
std::cout << "unique_lock测试后counter = " << counter << std::endl;
std::cout << "\n=== 递归锁测试 ===" << std::endl;
std::thread t3(recursive_func, 3);
t3.join();
return 0;
}
4.3 死锁详解
4.3.1 死锁的四个必要条件
-
互斥条件:资源不能被共享,一次只能一个线程使用
-
占有并等待:线程已持有至少一个资源,又请求其他被占有的资源
-
不可剥夺:资源不能被强行抢占,只能由持有者主动释放
-
循环等待:存在线程资源的循环等待链
四个条件同时满足时才会发生死锁,破坏任意一个即可避免死锁。
4.3.2 死锁演示与避免方法
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mutex_a; // 锁A
std::mutex mutex_b; // 锁B
// ==========================================
// 死锁版本:两个线程以相反顺序加锁
// ==========================================
void thread1_deadlock() {
std::cout << "线程1:尝试获取锁A" << std::endl;
std::lock_guard<std::mutex> lock1(mutex_a); // 先拿A
std::cout << "线程1:已获取锁A" << std::endl;
// 休眠一下,让线程2有机会拿到锁B
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "线程1:尝试获取锁B" << std::endl;
std::lock_guard<std::mutex> lock2(mutex_b); // 再拿B
std::cout << "线程1:已获取锁B" << std::endl;
std::cout << "线程1:工作完成" << std::endl;
}
void thread2_deadlock() {
std::cout << "线程2:尝试获取锁B" << std::endl;
std::lock_guard<std::mutex> lock1(mutex_b); // 先拿B
std::cout << "线程2:已获取锁B" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "线程2:尝试获取锁A" << std::endl;
std::lock_guard<std::mutex> lock2(mutex_a); // 再拿A
std::cout << "线程2:已获取锁A" << std::endl;
std::cout << "线程2:工作完成" << std::endl;
}
// ==========================================
// 避免死锁方法1:统一加锁顺序
// ==========================================
void thread1_safe() {
std::lock_guard<std::mutex> lock1(mutex_a); // 都先拿A
std::lock_guard<std::mutex> lock2(mutex_b); // 再拿B
std::cout << "线程1:安全获取两把锁" << std::endl;
}
void thread2_safe() {
std::lock_guard<std::mutex> lock1(mutex_a); // 都先拿A
std::lock_guard<std::mutex> lock2(mutex_b); // 再拿B
std::cout << "线程2:安全获取两把锁" << std::endl;
}
// ==========================================
// 避免死锁方法2:std::lock同时加锁
// ==========================================
void thread_std_lock1() {
// std::lock可以同时锁定多个互斥量
// 它内部会用特殊算法避免死锁
std::lock(mutex_a, mutex_b);
// adopt_lock表示已经加锁了,lock_guard只负责析构时解锁
std::lock_guard<std::mutex> lock1(mutex_a, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mutex_b, std::adopt_lock);
std::cout << "线程1:std::lock同时获取两把锁" << std::endl;
}
void thread_std_lock2() {
std::lock(mutex_a, mutex_b);
std::lock_guard<std::mutex> lock1(mutex_a, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mutex_b, std::adopt_lock);
std::cout << "线程2:std::lock同时获取两把锁" << std::endl;
}
int main() {
std::cout << "=== 注意:死锁演示会导致程序卡住 ===" << std::endl;
std::cout << "这里只展示安全版本的代码" << std::endl;
std::cout << "\n--- 方法1:统一加锁顺序 ---" << std::endl;
std::thread t1(thread1_safe);
std::thread t2(thread2_safe);
t1.join();
t2.join();
std::cout << "\n--- 方法2:std::lock同时加锁 ---" << std::endl;
std::thread t3(thread_std_lock1);
std::thread t4(thread_std_lock2);
t3.join();
t4.join();
std::cout << "\n所有操作完成,无死锁" << std::endl;
return 0;
}
4.4 条件变量(Condition Variable)
4.4.1 核心原理
条件变量用于线程间的协作:一个线程等待某个条件成立,另一个线程在条件满足时发出通知。条件变量必须和互斥锁配合使用。
典型应用场景:
-
生产者-消费者模型
-
线程等待某个事件发生
-
任务队列的等待唤醒机制
工作机制:
-
等待线程:先加锁 → 检查条件 → 不满足则调用wait(自动解锁+阻塞)→ 被唤醒后重新加锁 → 再次检查条件
-
通知线程:加锁 → 修改条件 → 发送通知 → 解锁
为什么要用while循环检查条件?
因为存在虚假唤醒(spurious wakeup):条件变量可能在没有被notify的情况下自行唤醒。所以唤醒后必须重新检查条件是否真的满足。
4.4.2 C++条件变量完整示例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable> // 条件变量头文件
#include <queue>
#include <string>
// ==========================================
// 生产者-消费者模型
// ==========================================
std::mutex mtx; // 保护共享数据的互斥锁
std::condition_variable cv; // 条件变量
std::queue<int> task_queue; // 任务队列(共享数据)
bool finished = false; // 生产结束标志
// 生产者线程:生产数据放入队列
void producer(int id, int count) {
for (int i = 1; i <= count; i++) {
// 模拟生产耗时
std::this_thread::sleep_for(std::chrono::milliseconds(200));
{
// 操作共享数据前加锁
std::lock_guard<std::mutex> lk(mtx);
// 将任务放入队列
task_queue.push(id * 100 + i);
std::cout << "生产者" << id << ": 生产了任务 " << (id * 100 + i) << std::endl;
} // 解锁
// 通知一个等待的消费者线程
// notify_one唤醒一个等待线程
// notify_all唤醒所有等待线程
cv.notify_one();
}
std::cout << "生产者" << id << ": 生产完毕" << std::endl;
}
// 消费者线程:从队列取数据处理
void consumer(int id) {
while (true) {
// 条件变量必须配合std::unique_lock使用
// 因为wait内部需要能手动解锁和加锁
std::unique_lock<std::mutex> lk(mtx);
// wait等待条件满足
// 第二个参数是谓词(返回bool的可调用对象)
// 谓词为false时继续等待,为true时停止等待
// 等价于:while (!pred()) { wait(lk); }
// wait内部会自动解锁互斥量,让其他线程可以修改条件
// 被唤醒后会重新加锁,然后再次检查谓词
cv.wait(lk, []{
return !task_queue.empty() || finished;
});
// 到这里说明:队列非空 或 生产结束
if (!task_queue.empty()) {
// 取出任务
int task = task_queue.front();
task_queue.pop();
// 取出数据后可以提前解锁,减少锁持有时间
lk.unlock();
// 处理任务(模拟耗时)
std::cout << " 消费者" << id << ": 处理任务 " << task << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
else if (finished) {
// 生产结束且队列为空,退出循环
std::cout << " 消费者" << id << ": 收到结束信号,退出" << std::endl;
break;
}
}
}
int main() {
std::cout << "===== 生产者-消费者模型演示 =====" << std::endl;
// 创建2个生产者,各生产5个任务
std::thread prod1(producer, 1, 5);
std::thread prod2(producer, 2, 5);
// 创建2个消费者
std::thread cons1(consumer, 1);
std::thread cons2(consumer, 2);
// 等待所有生产者完成
prod1.join();
prod2.join();
// 所有生产者完成后,设置结束标志并通知所有消费者
{
std::lock_guard<std::mutex> lk(mtx);
finished = true;
std::cout << "\n主线程:所有生产完毕,通知消费者退出" << std::endl;
}
cv.notify_all(); // 唤醒所有等待的消费者
// 等待消费者结束
cons1.join();
cons2.join();
std::cout << "\n所有线程执行完毕" << std::endl;
return 0;
}
4.5 读写锁(Reader-Writer Lock)
4.5.1 核心原理
读写锁适用于"读多写少"的场景。它将访问分为两种:
-
读锁(共享锁):多个线程可以同时持有读锁,并发读取
-
写锁(独占锁):写锁是独占的,持有写锁时不能有任何其他读锁或写锁
规则总结:
-
读-读:不互斥,可以并发
-
读-写:互斥,写阻塞读,读也阻塞写
-
写-写:互斥
相比互斥锁,读写锁在读多写少场景下并发性能更好。
4.5.2 C语言读写锁示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
int shared_data = 0; // 共享数据
pthread_rwlock_t rwlock; // 读写锁
// 读者线程:读取数据
void* reader(void* arg) {
long id = (long)arg;
for (int i = 0; i < 5; i++) {
// 获取读锁(共享锁)
// 多个读者可以同时获得读锁,并行读取
// 如果当前有写锁,则阻塞等待写锁释放
pthread_rwlock_rdlock(&rwlock);
// 临界区:读取共享数据
printf("读者%ld: 读取数据 = %d\n", id, shared_data);
// 模拟读取耗时
// 因为读锁是共享的,多个读者会同时停在这里
usleep(100000); // 100毫秒
// 释放读锁
pthread_rwlock_unlock(&rwlock);
usleep(200000); // 读取间隔
}
return NULL;
}
// 写者线程:修改数据
void* writer(void* arg) {
long id = (long)arg;
for (int i = 0; i < 3; i++) {
// 获取写锁(独占锁)
// 写锁与所有锁都互斥
// 必须等所有读锁和写锁都释放才能获取
pthread_rwlock_wrlock(&rwlock);
// 临界区:修改共享数据
shared_data++;
printf(" 写者%ld: 修改数据为 %d\n", id, shared_data);
usleep(200000); // 模拟写入耗时
// 释放写锁
pthread_rwlock_unlock(&rwlock);
usleep(300000); // 写入间隔
}
return NULL;
}
int main() {
pthread_t r1, r2, r3, r4; // 4个读者
pthread_t w1, w2; // 2个写者
// 初始化读写锁
// 参数2:锁属性,NULL使用默认
pthread_rwlock_init(&rwlock, NULL);
printf("程序启动:4个读者 + 2个写者\n");
printf("观察:读者可以并发读取,写者独占\n\n");
// 创建读者线程
pthread_create(&r1, NULL, reader, (void*)1);
pthread_create(&r2, NULL, reader, (void*)2);
pthread_create(&r3, NULL, reader, (void*)3);
pthread_create(&r4, NULL, reader, (void*)4);
// 创建写者线程
pthread_create(&w1, NULL, writer, (void*)1);
pthread_create(&w2, NULL, writer, (void*)2);
// 等待所有线程结束
pthread_join(r1, NULL);
pthread_join(r2, NULL);
pthread_join(r3, NULL);
pthread_join(r4, NULL);
pthread_join(w1, NULL);
pthread_join(w2, NULL);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
printf("\n所有线程执行完毕\n");
return 0;
}
4.6 信号量(Semaphore)
4.6.1 核心原理
信号量是一个计数器,支持两种原子操作:
-
P操作(wait/acquire):计数器减1,如果减到负数则阻塞等待
-
V操作(post/release):计数器加1,如果有线程等待则唤醒一个
信号量分类:
-
二值信号量:计数器最大值为1,功能类似互斥锁
-
计数信号量:计数器可以是任意正整数,用于控制并发数量
信号量与互斥锁的区别:
-
互斥锁必须由加锁的线程解锁,信号量可以由任意线程释放
-
互斥锁保护临界区,信号量用于同步和计数
-
信号量可以实现更复杂的同步模式
4.6.2 POSIX信号量实现生产者消费者
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h> // POSIX信号量头文件
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_SIZE 5 // 缓冲区大小
int buffer[BUFFER_SIZE]; // 环形缓冲区
int in = 0; // 写入位置指针
int out = 0; // 读取位置指针
sem_t empty_sem; // 空位信号量,表示还有多少空位可以放数据
sem_t full_sem; // 满位信号量,表示有多少数据可以取
sem_t mutex_sem; // 二值信号量,保护缓冲区操作(替代互斥锁)
// 生产者线程
void* producer(void* arg) {
long id = (long)arg;
int item = 1;
for (int i = 0; i < 8; i++) {
// P操作:等待空位
// empty_sem减1,如果为0则阻塞
// 确保缓冲区不满
sem_wait(&empty_sem);
// P操作:获取缓冲区访问权
// 这是二值信号量,作用等同于互斥锁
sem_wait(&mutex_sem);
// ===== 临界区:写入数据 =====
buffer[in] = id * 100 + item;
printf("生产者%ld: 放入 %d (位置%d)\n", id, buffer[in], in);
in = (in + 1) % BUFFER_SIZE;
item++;
// ============================
// V操作:释放缓冲区
sem_post(&mutex_sem);
// V操作:满位加1
// 通知消费者有新数据了
sem_post(&full_sem);
usleep(150000); // 生产间隔
}
printf("生产者%ld: 生产完毕\n", id);
return NULL;
}
// 消费者线程
void* consumer(void* arg) {
long id = (long)arg;
for (int i = 0; i < 8; i++) {
// P操作:等待满位
// full_sem减1,如果为0则阻塞
// 确保缓冲区不空
sem_wait(&full_sem);
// P操作:获取缓冲区访问权
sem_wait(&mutex_sem);
// ===== 临界区:读取数据 =====
int data = buffer[out];
printf(" 消费者%ld: 取出 %d (位置%d)\n", id, data, out);
out = (out + 1) % BUFFER_SIZE;
// ============================
// V操作:释放缓冲区
sem_post(&mutex_sem);
// V操作:空位加1
// 通知生产者有空位了
sem_post(&empty_sem);
usleep(200000); // 消费间隔
}
printf(" 消费者%ld: 消费完毕\n", id);
return NULL;
}
int main() {
pthread_t prod1, prod2;
pthread_t cons1, cons2;
// 初始化信号量
// 参数1:信号量指针
// 参数2:0表示线程间共享,非0表示进程间共享
// 参数3:信号量初始值
sem_init(&empty_sem, 0, BUFFER_SIZE); // 初始有5个空位
sem_init(&full_sem, 0, 0); // 初始0个数据
sem_init(&mutex_sem, 0, 1); // 二值信号量,初始为1
printf("环形缓冲区大小: %d\n\n", BUFFER_SIZE);
// 创建2个生产者和2个消费者
pthread_create(&prod1, NULL, producer, (void*)1);
pthread_create(&prod2, NULL, producer, (void*)2);
pthread_create(&cons1, NULL, consumer, (void*)1);
pthread_create(&cons2, NULL, consumer, (void*)2);
// 等待所有线程
pthread_join(prod1, NULL);
pthread_join(prod2, NULL);
pthread_join(cons1, NULL);
pthread_join(cons2, NULL);
// 销毁信号量
sem_destroy(&empty_sem);
sem_destroy(&full_sem);
sem_destroy(&mutex_sem);
printf("\n程序结束\n");
return 0;
}
5. 线程间通信与数据共享
5.1 原子操作
5.1.1 核心原理
原子操作是指不可分割的操作,在执行完成前不会被其他线程打断。硬件层面保证原子性,不需要互斥锁,性能更好。
适用场景:
-
简单的计数器、标志位
-
无锁数据结构
-
高性能并发场景
C++ <atomic> 库提供了各种原子类型:
-
std::atomic<int>、std::atomic<long>等整型 -
std::atomic<bool>布尔型 -
std::atomic<T*>指针类型
基本原子操作:
-
load():原子读取 -
store():原子写入 -
fetch_add():原子加法,返回旧值 -
fetch_sub():原子减法 -
exchange():原子交换,返回旧值 -
compare_exchange_weak/strong():比较并交换(CAS)
5.1.2 完整示例:原子计数器
#include <iostream>
#include <thread>
#include <atomic> // 原子操作头文件
#include <vector>
// 原子整型变量
// 对它的所有操作都是原子的,不需要互斥锁
std::atomic<int> atomic_counter(0);
// 普通整型变量(对比用)
int normal_counter = 0;
void atomic_increment(int times) {
for (int i = 0; i < times; i++) {
// 原子自增操作,线程安全
// 等价于 atomic_counter.fetch_add(1)
atomic_counter++;
}
}
void normal_increment(int times) {
for (int i = 0; i < times; i++) {
// 非原子操作,有竞态条件
normal_counter++;
}
}
int main() {
const int thread_num = 10;
const int per_thread = 10000;
std::cout << "=== 对比:原子操作 vs 非原子操作 ===" << std::endl;
std::cout << thread_num << "个线程,每个累加" << per_thread << "次" << std::endl;
std::cout << "预期结果: " << thread_num * per_thread << std::endl;
std::vector<std::thread> threads;
// 测试原子计数器
for (int i = 0; i < thread_num; i++) {
threads.emplace_back(atomic_increment, per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout << "\n原子计数器结果: " << atomic_counter.load() << std::endl;
std::cout << "是否正确: " << (atomic_counter == thread_num * per_thread ? "是" : "否") << std::endl;
// 测试普通计数器
threads.clear();
for (int i = 0; i < thread_num; i++) {
threads.emplace_back(normal_increment, per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout << "\n普通计数器结果: " << normal_counter << std::endl;
std::cout << "是否正确: " << (normal_counter == thread_num * per_thread ? "是" : "否") << std::endl;
// ==========================================
// 更多原子操作演示
// ==========================================
std::cout << "\n=== 更多原子操作演示 ===" << std::endl;
std::atomic<int> val(10);
// fetch_add:原子加,返回旧值
int old = val.fetch_add(5);
std::cout << "fetch_add(5): 旧值=" << old << ", 新值=" << val.load() << std::endl;
// exchange:原子替换,返回旧值
old = val.exchange(100);
std::cout << "exchange(100): 旧值=" << old << ", 新值=" << val.load() << std::endl;
// CAS(比较并交换)
int expected = 100;
int desired = 200;
// 如果当前值等于expected,则设为desired,返回true
// 如果不等,则把expected更新为当前值,返回false
bool success = val.compare_exchange_strong(expected, desired);
std::cout << "CAS(100->200): " << (success ? "成功" : "失败")
<< ", 当前值=" << val.load() << std::endl;
// 测试失败的CAS
expected = 999;
success = val.compare_exchange_strong(expected, 9999);
std::cout << "CAS(999->9999): " << (success ? "成功" : "失败")
<< ", expected被更新为=" << expected << std::endl;
return 0;
}
5.2 内存序(Memory Order)
5.2.1 核心概念
现代CPU和编译器为了性能会对指令进行重排序。单线程下只要最终结果一致,重排序是允许的。但多线程环境下,重排序可能导致一个线程看到另一个线程的操作顺序与代码顺序不一致。
内存序就是用来控制内存可见性和指令重排序的。
六种内存序(从弱到强):
-
memory_order_relaxed:松散序,只保证原子性,不保证顺序 -
memory_order_consume:消费序,数据依赖的读不重排到此读之前 -
memory_order_acquire:获取序,此操作之后的读写不能重排到它之前 -
memory_order_release:释放序,此操作之前的读写不能重排到它之后 -
memory_order_acq_rel:获取-释放序,同时具有acquire和release语义 -
memory_order_seq_cst:顺序一致性,最强保证(默认值)
常用模式:
- release-acquire配对:一个线程用release写,另一个线程用acquire读。保证写线程release之前的所有写操作,对读线程acquire之后的读操作都可见。
5.2.2 示例:release-acquire 同步
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
// 共享数据
int data = 0;
// 同步标志
std::atomic<bool> ready(false);
// 写线程:先写数据,再设置标志
void writer_thread() {
// 准备数据
data = 42;
data += 100;
// ... 其他数据写入 ...
// release语义:保证之前的所有写操作都完成
// 并且对后续acquire读取此变量的线程可见
// 简单说:release之前的写,不能重排到release之后
ready.store(true, std::memory_order_release);
std::cout << "写线程:数据已准备好" << std::endl;
}
// 读线程:先读标志,再读数据
void reader_thread() {
// 自旋等待,使用acquire语义
// acquire语义:保证之后的读操作不会重排到acquire之前
// 简单说:acquire之后的读,不能重排到acquire之前
while (!ready.load(std::memory_order_acquire)) {
// 空转等待
}
// 到这里,保证能看到writer在release之前的所有写入
// data一定是142,不会看到未初始化或部分写入的值
std::cout << "读线程:读取到 data = " << data << std::endl;
if (data == 142) {
std::cout << "✓ release-acquire 同步成功,数据完整可见" << std::endl;
}
}
int main() {
std::cout << "=== release-acquire 内存序演示 ===" << std::endl;
std::cout << "写线程用release设置标志,读线程用acquire读取标志" << std::endl;
std::cout << "保证写线程release前的所有写入,对读线程acquire后可见\n" << std::endl;
std::thread writer(writer_thread);
std::thread reader(reader_thread);
writer.join();
reader.join();
// ==========================================
// 内存序总结
// ==========================================
std::cout << "\n=== 内存序选择建议 ===" << std::endl;
std::cout << "1. 只需要原子性,不关心顺序 → memory_order_relaxed" << std::endl;
std::cout << "2. 线程间同步数据 → release-acquire 配对" << std::endl;
std::cout << "3. 需要全局顺序一致 → memory_order_seq_cst(默认)" << std::endl;
std::cout << "4. 不确定用什么 → 默认seq_cst,性能不够再优化" << std::endl;
return 0;
}
5.3 Future 与 Promise
5.3.1 核心概念
std::future 和 std::promise 是C++提供的异步结果获取机制。简单说就是:一个线程(Promise端)设置结果,另一个线程(Future端)等待并获取结果。
三者关系:
-
std::promise:用于设置结果的"输入端"
-
std::future:用于获取结果的"输出端"
-
一个promise对应一个future,通过
promise.get_future()关联
三种使用方式:
-
std::async:最简单,直接异步执行函数,返回future -
std::promise+std::future:手动设置结果 -
std::packaged_task:包装任务,可在线程中执行
5.3.2 完整示例
#include <iostream>
#include <thread>
#include <future> // future, promise, async, packaged_task
#include <chrono>
#include <string>
// ==========================================
// 方式1:std::async 最简单的异步调用
// ==========================================
int calculate_sum(int a, int b) {
std::cout << " [calculate_sum] 开始计算 " << a << " + " << b << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时
return a + b;
}
void demo_async() {
std::cout << "=== std::async 演示 ===" << std::endl;
// std::async启动一个异步任务,返回std::future
// std::launch::async:立即创建新线程执行
// std::launch::deferred:延迟执行,等调用get时才在当前线程执行
std::future<int> result = std::async(std::launch::async, calculate_sum, 10, 20);
std::cout << "主线程:异步任务已启动,继续做别的事" << std::endl;
// 可以做其他工作...
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "主线程:准备获取结果..." << std::endl;
// get()阻塞等待结果就绪,然后返回结果
// 只能调用一次get
int value = result.get();
std::cout << "计算结果: " << value << std::endl;
}
// ==========================================
// 方式2:std::promise + std::future
// ==========================================
void worker_task(std::promise<std::string>& prom, int task_id) {
std::cout << " [worker] 任务" << task_id << "开始执行" << std::endl;
try {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (task_id < 0) {
// 也可以设置异常
throw std::runtime_error("任务ID无效");
}
// 设置结果,对应的future会收到通知
std::string result = "任务" + std::to_string(task_id) + "完成";
prom.set_value(result);
std::cout << " [worker] 任务" << task_id << "已设置结果" << std::endl;
} catch (...) {
// 设置异常,future端get时会抛出
prom.set_exception(std::current_exception());
}
}
void demo_promise() {
std::cout << "\n=== std::promise 演示 ===" << std::endl;
// 创建promise对象
std::promise<std::string> prom;
// 获取关联的future
std::future<std::string> fut = prom.get_future();
// 将promise传递给工作线程
// 注意:promise不能拷贝,必须用引用或移动
std::thread t(worker_task, std::ref(prom), 42);
std::cout << "主线程:等待任务结果..." << std::endl;
try {
// wait()只等待,不获取结果
fut.wait();
// get()获取结果(如果有异常会抛出)
std::string result = fut.get();
std::cout << "收到结果: " << result << std::endl;
} catch (const std::exception& e) {
std::cout << "捕获异常: " << e.what() << std::endl;
}
t.join();
}
// ==========================================
// 方式3:std::packaged_task
// ==========================================
int compute_square(int x) {
std::cout << " [compute_square] 计算 " << x << " 的平方" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
return x * x;
}
void demo_packaged_task() {
std::cout << "\n=== std::packaged_task 演示 ===" << std::endl;
// packaged_task包装一个可调用对象
// 它像一个函数,调用后将结果存入future
std::packaged_task<int(int)> task(compute_square);
// 获取future
std::future<int> fut = task.get_future();
// 将task移动到线程中执行
// packaged_task不可拷贝,只能移动
std::thread t(std::move(task), 8);
std::cout << "主线程:等待计算结果..." << std::endl;
int result = fut.get();
std::cout << "8的平方 = " << result << std::endl;
t.join();
}
int main() {
demo_async();
demo_promise();
demo_packaged_task();
std::cout << "\n=== 三种方式对比 ===" << std::endl;
std::cout << "std::async: 最简单,适合简单异步调用" << std::endl;
std::cout << "std::promise: 最灵活,手动控制何时设置结果" << std::endl;
std::cout << "std::packaged_task: 包装任务,适合线程池等场景" << std::endl;
return 0;
}
6. 高级线程技术
6.1 线程局部存储(TLS)
6.1.1 核心概念
线程局部存储(Thread Local Storage,TLS)让每个线程拥有变量的独立副本。线程之间互不影响,不需要同步。
适用场景:
-
线程私有的状态信息
-
避免频繁分配的线程私有缓冲区
-
错误码、随机数种子等按线程隔离的数据
C语言使用 pthread_key_t 机制,C++11提供了 thread_local 关键字。
6.1.2 C++ thread_local 示例
#include <iostream>
#include <thread>
#include <string>
// thread_local 关键字声明线程局部变量
// 每个线程都有自己独立的副本
// 线程创建时初始化,线程结束时销毁
thread_local int thread_id = 0;
thread_local std::string thread_name;
// 普通全局变量(对比用)
int global_counter = 0;
void worker(int id, const std::string& name) {
// 设置线程局部变量
thread_id = id;
thread_name = name;
// 修改全局变量
global_counter++;
std::cout << name << ": thread_id = " << thread_id
<< " (地址: " << &thread_id << ")" << std::endl;
std::cout << name << ": thread_name = " << thread_name
<< " (地址: " << &thread_name << ")" << std::endl;
std::cout << name << ": global_counter = " << global_counter
<< " (地址: " << &global_counter << ")" << std::endl;
// 修改线程局部变量
thread_id += 1000;
std::cout << name << ": 修改后 thread_id = " << thread_id << std::endl;
std::cout << "---" << std::endl;
}
int main() {
std::cout << "=== 线程局部存储演示 ===" << std::endl;
std::cout << "每个thread_local变量在线程中有独立副本,地址不同\n" << std::endl;
std::thread t1(worker, 1, "线程A");
std::thread t2(worker, 2, "线程B");
std::thread t3(worker, 3, "线程C");
t1.join();
t2.join();
t3.join();
// 主线程也有自己的thread_local副本
std::cout << "主线程: 自己的thread_id = " << thread_id << std::endl;
std::cout << "主线程: global_counter = " << global_counter << std::endl;
std::cout << "\n总结:" << std::endl;
std::cout << "- thread_local变量:每个线程独立副本,互不影响" << std::endl;
std::cout << "- 普通全局变量:所有线程共享,需要同步保护" << std::endl;
return 0;
}
6.2 线程屏障(Barrier)
6.2.1 核心概念
屏障(Barrier)是一种同步机制,让一组线程在某个点集合,所有线程都到达后再一起继续执行。
典型应用:
-
分阶段计算,确保所有线程完成上一阶段再进入下一阶段
-
多线程初始化,等所有线程初始化完成再开始工作
C语言使用 pthread_barrier_t,C++20引入了 std::barrier。
6.2.2 pthread_barrier 示例
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define THREAD_COUNT 3
pthread_barrier_t barrier; // 屏障对象
// 分阶段工作的线程函数
void* phase_worker(void* arg) {
long id = (long)arg;
// ===== 第一阶段工作 =====
printf("线程%ld: 开始第一阶段初始化\n", id);
sleep(id + 1); // 模拟不同的工作时间
printf("线程%ld: 第一阶段完成,等待其他线程...\n", id);
// 屏障等待
// 所有线程都到达这里后,才一起继续往下执行
// 没到齐之前,先到的线程阻塞在这里
pthread_barrier_wait(&barrier);
printf("线程%ld: 所有线程就绪,开始第二阶段工作\n", id);
sleep(1);
printf("线程%ld: 第二阶段完成,再次等待...\n", id);
// 可以重复使用屏障
pthread_barrier_wait(&barrier);
printf("线程%ld: 开始第三阶段(最终阶段)\n", id);
sleep(1);
printf("线程%ld: 全部工作完成\n", id);
return NULL;
}
int main() {
pthread_t threads[THREAD_COUNT];
// 初始化屏障
// 参数2:屏障计数,即需要等待的
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)