代码仓库shanchuann/CPP-Learninng

在C++11标准推出之前,C++开发者实现多线程编程只能依赖平台原生API,例如Linux环境下的pthread线程库,此类方式编码流程繁琐、上手门槛较高,且存在平台兼容性问题,跨平台移植难度较大。C++11首次在语言层面引入标准多线程库,通过头文件对底层线程操作进行封装,既简化了多线程开发流程,也保留了Linux线程的底层运行逻辑与内核原理。本文系统梳理C++11 thread的相关概念、实现方法、底层逻辑与使用注意事项,完整覆盖多线程基础与进阶技术内容,适配常规开发与技术学习需求。

基础知识

依赖头文件

使用C++11标准线程相关功能,需提前包含对应头文件,头文件是线程类、同步工具及相关函数的定义载体,缺失相关头文件会直接引发编译报错,基础多线程开发需包含的核心头文件如下:

1
2
3
4
5
6
7
8
#include <iostream>
// C++11标准线程核心头文件,封装thread类
#include <thread>
// 线程同步互斥锁所需头文件
#include <mutex>
// 条件变量配套头文件,适用于高级同步场景
#include <condition_variable>
using namespace std;

C++11线程与Linux原生线程

C++11 thread与Linux线程底层内核逻辑一致,二者均属于内核级线程,可共享进程全局资源,同时各自拥有独立的内核栈与线程上下文;二者的主要区别在于,C++11对Linux原生pthread等底层API进行了高层封装,屏蔽了不同平台的底层差异,开发者无需手动处理线程句柄、资源销毁等细节,编码效率高于原生Linux pthread编程,底层仍依托系统内核完成线程调度,易用性与跨平台兼容性有所提升。

线程对象特性

thread类的设计遵循特定规范,该规范可规避多线程对象共享内核资源引发的资源冲突、程序异常崩溃等问题,也是现代多线程编程的通用设计准则,相关特性在源码设计与实际使用中均有明确体现。

构造与赋值特性

  • 禁止拷贝构造、拷贝赋值:thread类在源码中显式删除拷贝构造函数与拷贝赋值运算符,不支持线程对象之间的相互拷贝。原因在于每个线程对象对应唯一的内核级资源,包含独立内核栈与线程上下文,若允许拷贝操作,会出现多个线程对象管理同一份内核资源的情况,进而引发资源重复释放、程序运行异常等问题。
  • 支持移动构造、移动赋值:线程对象支持移动语义,可通过资源所有权转移的方式,将原线程对象的内核资源转移至新线程对象,而非复制现有资源。移动操作完成后,原线程对象会转为空态,也可称为幼稚态,不再具备内核资源权限,无法执行join、获取线程ID等操作,也无需额外进行资源回收处理。

技术提示:不同平台的线程对象移动后行为存在差异,Windows平台下线程对象移动后,底层指针不会置空,会转为随机无效值,此时调用join会触发运行时错误;Linux平台部分实现会将底层指针置空,开发过程中应避免使用移动后的原线程对象,减少跨平台兼容性隐患。

线程内核资源管理

每个线程对象对应系统分配的专属内核栈与线程上下文,内核栈是线程独立运行的核心内存区域,整体分为两部分:头部信息区用于存储线程ID、寄存器状态、运行状态等上下文数据,实际栈空间用于存储线程函数的局部变量与参数副本。内核栈大小随操作系统不同存在差异,常规范围为1MB-10MB,各线程的内核栈相互独立,可避免局部参数冲突问题。

线程创建与资源管理

创建语法

C++11线程创建逻辑简洁,无需额外配置线程属性,thread构造函数首个参数为线程入口函数地址,直接传入函数名即可,后续参数依次为入口函数所需实参,基础语法清晰,上手难度较低,对应示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 线程入口函数,线程启动后执行该函数逻辑
void funA(int val) {
for (int i = 0; i < 5; ++i) {
cout << "线程执行,传入参数值:" << val << endl;
}
}

int main() {
// 创建线程对象t1,绑定入口函数funA,传入参数1
thread t1(funA, 1);
// 等待子线程执行完毕,回收内核资源
t1.join();
return 0;
}

资源回收

join()是线程资源管理的常用函数,调用该函数与程序稳定性、系统资源安全直接相关,相关要点如下:

  • 功能作用:阻塞当前线程,等待子线程执行完毕后再继续后续逻辑,同时自动回收线程对应的内核栈、上下文等内核资源,减少内存泄漏风险;
  • 未调用影响:若不调用join(),主线程提前退出时,子线程内核资源无法被系统正常回收,可能引发内存泄漏,严重时会导致程序异常退出;
  • 使用限制:仅具备有效内核资源的线程对象可调用join(),空态线程调用该函数会触发运行时错误。

线程分离

detach()函数可实现线程与主线程的分离,分离后的线程转为后台守护线程,执行完毕后由系统自动释放内核资源,无需手动调用join()。该函数存在使用限制,若主线程执行完毕并退出程序,关联的分离线程会被系统强制回收资源,即便子线程未完成业务逻辑,也会被终止运行,导致业务逻辑失效。因此detach()适用于无严格执行顺序、无数据依赖的简单场景。

线程ID获取

线程ID是线程的唯一标识,可用于区分不同线程、排查线程调度异常问题,有两种等效获取方式,返回结果一致,适用场景有所区别:

  • 线程外部获取:通过线程对象调用get_id()方法,适用于外部查看线程ID;
  • 线程内部获取:在线程函数内部调用std::this_thread::get_id(),可获取当前执行线程的ID,适用于线程内部逻辑调试。

空态线程获取ID时,会返回固定无效ID值,可通过该特性判断线程是否具备有效内核资源,排查空态线程误用问题。

线程函数参数传递

线程函数参数传递是C++11 thread的重点内容,主要分为值传递、引用传递、右值引用三种方式,各方式底层逻辑、语法规则与适用场景不同。基础前提为每个线程拥有独立内核栈,参数会生成独立副本,同名变量的内存地址不同,不会出现多线程共享局部参数的情况,可降低数据冲突概率。

值传递

值传递是线程参数的默认传递方式,直接传入实参即可,线程内部会通过拷贝构造生成参数副本,存储于专属内核栈中,线程内对参数副本的修改不会影响主线程原变量。该方式稳定性较强,适用于无需修改原变量、数据量较小的常规场景。

常见误区:部分使用者会误认为多线程共用同一参数变量,实际各线程的参数均为独立副本,即便变量名相同,内存地址也相互独立,不会出现参数覆盖、数据错乱的情况。

引用传递

若需在线程内修改主线程原变量,实现数据双向同步,可采用引用传递,直接传入变量无法完成编译,原因是线程构造时会生成右值副本,普通左值引用无法绑定右值。需通过std::ref对变量进行包装,将参数转为引用类型传递,线程内操作的变量为主线程原变量的别名,修改结果会同步至原变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 线程入口函数,参数为引用类型
void funB(int& val) {
// 修改引用参数,对应主线程原变量
val = 100;
}

int main() {
int num = 10;
// 通过std::ref包装变量,完成引用传递
thread t2(funB, ref(num));
t2.join();
// 输出结果为100,原变量已被修改
cout << "主线程变量值:" << num << endl;
return 0;
}

右值引用与移动语义

针对字符串、容器、自定义大型对象等大数据量传参场景,值传递会触发深拷贝,占用较多内存与CPU资源,影响程序运行效率。此时可采用右值引用搭配移动语义的方式,通过std::move将左值转为右值,线程内部通过移动构造转移资源所有权,而非复制完整数据,提升大数据量传参效率。

移动操作完成后,主线程原对象失去资源所有权,转为空态,不可再进行读写操作,该方式适用于大数据量传参、无需保留原对象的场景。

参数传递选择:无需修改原变量可选用值传递;需要修改原变量可选用std::ref引用传递;大数据量对象传参可选用移动语义,减少深拷贝带来的性能损耗。

线程状态机模型

C++11线程的状态模型与Linux进程、线程状态保持一致,逻辑贴合通用进程状态模型,可归纳为四大核心状态,状态转换遵循固定流程,是理解线程调度、排查线程阻塞问题的基础:

  1. 就绪态:线程创建完成,内核资源分配完毕,等待CPU调度执行;
  2. 执行态:线程获取CPU时间片,执行入口函数内的业务逻辑;
  3. 阻塞态:线程等待锁、IO资源、条件满足等外部触发条件,主动放弃CPU使用权,进入阻塞状态,待资源就绪后由系统唤醒;
  4. 睡眠态:线程主动休眠指定时长,休眠时间到达后自动转为就绪态,等待CPU调度。

线程状态机模型

状态转换规则:阻塞态与睡眠态无法直接跳转至执行态,需先回到就绪态,再由CPU统一调度进入执行态;从阻塞态到就绪态的转换,需依靠系统主动发送通知,无法自行完成跳转。

多线程竞态与锁机制

竞态条件

多线程并发场景下,操作全局变量、标准输出cout、共享文件、堆内存等共享资源时,会出现竞态条件。线程执行顺序由CPU内核随机调度,无法人为干预执行优先级,易引发输出结果乱序、数据读写错乱、数值计算偏差等问题。例如多线程同时循环打印不同字符,控制台输出内容会呈现无序拼接状态,且每次运行结果存在差异,此类问题需通过锁机制实现线程同步解决。

C++11标准库锁

C++11在<mutex>头文件中封装了互斥锁体系,底层基于操作系统锁实现,屏蔽了平台差异,主要作用是实现线程间互斥访问共享资源,保证同一时间仅有一个线程操作临界区资源,缓解竞态条件带来的影响。C++11锁包含四类常用类型,各类型适用场景存在差异,具体说明如下:

std::mutex基础互斥锁

std::mutex属于常用的独占锁,同一时间仅能被一个线程持有,其他线程竞争锁时会进入阻塞态,直至持有锁的线程释放锁,该锁不支持递归与重入,同一线程重复加锁易引发死锁。基础操作包含lock()(阻塞加锁)、unlock()(主动解锁)、try_lock()(非阻塞尝试加锁,获取失败直接返回false,不会阻塞线程)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 全局互斥锁,保证多线程共享同一锁资源
mutex mtx;

void printChar(char ch) {
// 阻塞加锁,未获取到锁则线程阻塞
mtx.lock();
for (int i = 0; i < 10; ++i) {
cout << ch;
}
cout << endl;
// 主动解锁,释放锁资源
mtx.unlock();
}

int main() {
thread t3(printChar, 'A');
thread t4(printChar, 'B');
t3.join();
t4.join();
return 0;
}

使用注意:互斥锁不建议定义为线程函数内的局部变量,否则各线程会创建独立锁对象,无法实现线程间互斥,需定义为全局变量、静态变量或共享指针指向的锁,保障多线程共享同一把锁。

std::recursive_mutex递归锁

该锁针对递归函数、嵌套调用的多线程场景设计,支持同一线程多次重复加锁,不会直接引发死锁,加锁次数与解锁次数需保持一致,锁完全释放后,其他线程方可正常竞争。适用于函数嵌套调用、递归逻辑中需反复访问共享资源的场景,资源开销略高于基础mutex。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

recursive_mutex rec_mtx;
int shared_count = 0;

// 递归函数,同一线程多次加锁
void recursive_increment(int depth) {
if (depth <= 0) return;
rec_mtx.lock();
shared_count++;
cout << "递归深度 " << depth << ",当前计数 " << shared_count << endl;
recursive_increment(depth - 1);
rec_mtx.unlock();
}

int main() {
thread t(recursive_increment, 3);
t.join();
cout << "最终计数 " << shared_count << endl;
return 0;
}

std::timed_mutex定时锁

在基础mutex基础上新增超时等待功能,扩展方法包含try_lock_for(时间段)、try_lock_until(时间点),线程尝试加锁时,若锁被占用,仅会阻塞指定时长,超时未获取到锁则自动返回false,避免线程长时间阻塞,适用于对执行时效有要求的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;

timed_mutex timed_mtx;

// 持有锁的线程,长时间占用锁
void hold_lock() {
timed_mtx.lock();
cout << "线程1持有锁,休眠3秒" << endl;
this_thread::sleep_for(chrono::seconds(3));
timed_mtx.unlock();
}

// 尝试获取锁的线程,超时后返回
void try_get_lock() {
this_thread::sleep_for(chrono::milliseconds(500));
cout << "线程2尝试获取锁,等待1秒" << endl;
if (timed_mtx.try_lock_for(chrono::seconds(1))) {
cout << "线程2获取锁成功" << endl;
timed_mtx.unlock();
} else {
cout << "线程2等待超时,未获取到锁" << endl;
}
}

int main() {
thread t1(hold_lock);
thread t2(try_get_lock);
t1.join();
t2.join();
return 0;
}

std::shared_mutex读写锁

该锁实现读共享、写互斥的机制,分为共享锁与独占锁两种模式,共享锁用于读取操作,独占锁用于写入操作;多个线程可同时加读锁,并发读取共享资源,不影响运行效率;写锁为独占模式,加写锁时不允许其他线程加读锁或写锁,适用于读多写少的高并发场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <chrono>
using namespace std;

shared_mutex rw_mtx;
int shared_data = 0;

// 读线程,加共享锁,可并发执行
void read_func(int id) {
for (int i = 0; i < 3; ++i) {
rw_mtx.lock_shared();
cout << "读线程 " << id << " 读取数据:" << shared_data << endl;
this_thread::sleep_for(chrono::milliseconds(100));
rw_mtx.unlock_shared();
this_thread::sleep_for(chrono::milliseconds(200));
}
}

// 写线程,加独占锁,阻塞其他读写操作
void write_func() {
for (int i = 0; i < 2; ++i) {
rw_mtx.lock();
shared_data++;
cout << "写线程修改数据:" << shared_data << endl;
this_thread::sleep_for(chrono::milliseconds(300));
rw_mtx.unlock();
this_thread::sleep_for(chrono::milliseconds(500));
}
}

int main() {
thread w(write_func);
thread r1(read_func, 1);
thread r2(read_func, 2);
w.join();
r1.join();
r2.join();
return 0;
}

锁的使用

锁的使用并非简单的加锁与解锁操作,需遵循对应的规范准则,规范的使用方式直接影响多线程程序的运行稳定性、并发效率与系统资源安全,也是多线程开发中需要关注的重点内容,以下结合原理与实操逻辑,对核心规则进行说明。

锁的对称性

锁的对称性要求加锁与解锁操作保持对等,生命周期相互绑定,操作主体保持统一,该规则覆盖调用时机、异常场景、线程归属等全流程,打破对称性易引发死锁、资源泄露、程序崩溃等问题,相关规范与注意事项如下:

规范与注意事项

  • 基础对称要求:同一线程内,每次成功完成加锁操作,需对应一次解锁操作,避免出现加解锁次数失衡的情况。非递归锁不支持同一线程重复加锁,重复操作会导致线程陷入阻塞,影响程序正常运行。
  • 异常场景处理:若临界区内代码存在触发异常的可能,不建议使用手动加解锁方式,代码触发异常后会跳转至异常处理逻辑,可能跳过解锁步骤,导致锁资源被持续占用。依托RAII机制的锁管理器,可通过对象析构函数自动解锁,规避异常跳转导致的漏解锁问题。
  • 递归锁对称要求:递归锁支持同一线程多次重入加锁,仍需遵守加解锁次数一致的规则,仅当最后一次解锁完成后,锁资源才会完全释放,漏解锁仍会造成资源占用问题。
  • 线程归属要求:加锁与解锁操作需由同一线程完成,不建议跨线程执行加解锁操作,此类行为会破坏锁的内核状态与同步逻辑,引发程序未定义行为。

以下代码基于C++11标准实现,展示对称操作的正确写法与违规示例,适配常规开发环境。

错误示例:手动锁搭配异常场景漏解锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

mutex mtx;

// 手动加锁,临界区触发异常后无法正常解锁,锁资源被占用
void unsafe_func(int val) {
mtx.lock();
cout << "线程加锁成功,执行业务逻辑" << endl;
// 模拟业务异常,程序跳转后跳过unlock步骤
if (val == 0) throw runtime_error("业务异常触发");
mtx.unlock();
}

int main() {
try {
thread t1(unsafe_func, 0);
t1.join();
} catch (...) {
cout << "捕获异常,锁资源已泄露" << endl;
}
return 0;
}
正确示例:RAII锁管理器保障对称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <thread>
#include <mutex>
#include <stdexcept>
using namespace std;

mutex mtx;

// 借助lock_guard,构造时自动加锁,析构时自动解锁
void safe_func(int val) {
lock_guard<mutex> lock(mtx);
cout << "线程加锁成功,执行业务逻辑" << endl;
if (val == 0) throw runtime_error("业务异常触发");
// 离开作用域后自动解锁,无需手动操作
}

int main() {
try {
thread t1(safe_func, 0);
t1.join();
} catch (...) {
cout << "捕获异常,锁资源正常释放" &lt;&lt; endl;
}
return 0;
}
正确示例:递归锁对称使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

// 递归锁,支持同一线程重入
recursive_mutex rec_mtx;

// 递归函数,加锁与解锁次数保持一致
void recursive_test(int count) {
if (count <= 0) return;
rec_mtx.lock();
cout << "递归层数:" << count << ",加锁成功" << endl;
recursive_test(count - 1);
rec_mtx.unlock();
}

int main() {
thread t1(recursive_test, 3);
t1.join();
cout << "递归锁使用完毕,资源正常释放" << endl;
return 0;
}

锁的粒度把控

锁的粒度指锁保护的共享资源范围与临界区代码长度,是平衡多线程并发效率与资源安全的关键参数,粒度把控不当,会影响并发性能或引发资源安全隐患。核心逻辑为锁的粒度与并发度呈反向关系,粒度越大,临界区范围越广,同一时间可执行的线程数量越少,并发效率越低,但管理逻辑相对简单;粒度越小,临界区越短,线程等待锁的时间越短,并发效率越高,对资源划分的精细度要求也更高。

实操原则

  • 遵循最小粒度原则,仅锁定必要的共享资源与执行时长,缩小临界区范围,避免在临界区内执行文件IO、网络请求、休眠等耗时操作,减少单线程占用锁的时长。
  • 避免两种极端误区,一是过度扩大锁粒度,将大量无关代码放入临界区,丧失多线程并发优势;二是过度拆分锁资源,导致锁数量过多,增加CPU上下文切换开销,降低整体性能,需结合业务场景合理权衡。
错误示例:锁粒度过大,并发效率偏低
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;

mutex mtx;
int share_num = 0;

// 临界区包含耗时休眠与无关计算,长时间占用锁
void big_granularity() {
lock_guard<mutex> lock(mtx);
this_thread::sleep_for(chrono::seconds(1));
share_num++;
cout << "线程ID:" << this_thread::get_id() << ",数值:" << share_num << endl;
int temp = 0;
for (int i = 0; i < 1000; i++) temp += i;
}

int main() {
thread t1(big_granularity);
thread t2(big_granularity);
t1.join();
t2.join();
return 0;
}
正确示例:最小锁粒度,兼顾安全与并发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;

mutex mtx;
int share_num = 0;

// 耗时操作与无关代码置于锁外,仅锁定共享资源修改步骤
void small_granularity() {
this_thread::sleep_for(chrono::seconds(1));
int temp = 0;
for (int i = 0; i < 1000; i++) temp += i;

// 缩小临界区范围,快速释放锁
{
lock_guard<mutex> lock(mtx);
share_num++;
cout << "线程ID:" << this_thread::get_id() << ",数值:" << share_num << endl;
}
}

int main() {
thread t1(small_granularity);
thread t2(small_granularity);
t1.join();
t2.join();
return 0;
}

Linux原生线程锁

C++11锁底层依托Linux原生pthread锁实现,Linux作为多线程开发的常用平台,提供了多种底层锁机制,封装于<pthread.h>头文件中,支持内核态与用户态切换,适配底层开发场景,五类核心原生锁说明如下。

pthread_mutex_t基础互斥锁

Linux基础原生锁,对应C++11的std::mutex,属于独占锁,需手动完成初始化与销毁操作,核心API包含pthread_mutex_init()、pthread_mutex_lock()、pthread_mutex_unlock()、pthread_mutex_destroy(),默认属性为非递归,同一线程重复加锁易引发死锁,适用于常规线程互斥场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 全局基础互斥锁
pthread_mutex_t g_mutex;
// 共享资源
int g_num = 0;

// 线程入口函数
void* thread_func(void* arg) {
int i;
for (i = 0; i < 5; i++) {
pthread_mutex_lock(&g_mutex);
g_num++;
printf("线程%ld修改共享资源,当前值:%d\n", pthread_self(), g_num);
pthread_mutex_unlock(&g_mutex);
sleep(1);
}
return NULL;
}

int main() {
pthread_mutex_init(&g_mutex, NULL);
pthread_t t1, t2;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&g_mutex);
return 0;
}

递归互斥锁

通过修改pthread_mutex属性实现,设置PTHREAD_MUTEX_RECURSIVE属性后,支持同一线程多次重入加锁,对应C++11的std::recursive_mutex,适用于递归函数、嵌套调用场景,需配置属性后完成初始化,加锁与解锁次数需保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <pthread.h>

pthread_mutex_t g_rec_mutex;

void recursive_func(int count) {
if (count <= 0) return;
pthread_mutex_lock(&g_rec_mutex);
printf("递归加锁,当前层数:%d,线程ID:%ld\n", count, pthread_self());
recursive_func(count - 1);
pthread_mutex_unlock(&g_rec_mutex);
}

void* thread_func(void* arg) {
recursive_func(3);
return NULL;
}

int main() {
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&g_rec_mutex, &attr);

pthread_t t1;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_join(t1, NULL);

pthread_mutex_destroy(&g_rec_mutex);
pthread_mutexattr_destroy(&attr);
return 0;
}

pthread_rwlock_t读写锁

Linux原生读写锁,对应C++17的std::shared_mutex,实现读共享、写互斥机制,核心API包含pthread_rwlock_rdlock()、pthread_rwlock_wrlock()、pthread_rwlock_unlock(),支持多线程并发读取、单线程独占写入,适用于读多写少的高并发场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_rwlock_t g_rwlock;
int g_data = 0;

// 读线程,支持并发执行
void* read_thread(void* arg) {
while (1) {
pthread_rwlock_rdlock(&g_rwlock);
printf("读线程%ld读取数据:%d\n", pthread_self(), g_data);
pthread_rwlock_unlock(&g_rwlock);
sleep(1);
}
return NULL;
}

// 写线程,独占执行
void* write_thread(void* arg) {
while (1) {
pthread_rwlock_wrlock(&g_rwlock);
g_data++;
printf("写线程%ld修改数据:%d\n", pthread_self(), g_data);
pthread_rwlock_unlock(&g_rwlock);
sleep(2);
}
return NULL;
}

int main() {
pthread_rwlock_init(&g_rwlock, NULL);
pthread_t r1, r2, w1;
pthread_create(&r1, NULL, read_thread, NULL);
pthread_create(&r2, NULL, read_thread, NULL);
pthread_create(&w1, NULL, write_thread, NULL);

pthread_join(r1, NULL);
pthread_join(r2, NULL);
pthread_join(w1, NULL);

pthread_rwlock_destroy(&g_rwlock);
return 0;
}

pthread_spinlock_t自旋锁

自旋锁属于特殊锁类型,线程竞争锁失败时,不会进入内核阻塞态,而是在用户态循环尝试加锁,不会触发CPU上下文切换,资源开销较低,但会持续占用CPU。适用于临界区执行时间短、线程竞争不激烈的场景,C++11标准库未直接封装该锁类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_spinlock_t g_spinlock;
int g_count = 0;

void* spin_thread(void* arg) {
int i;
for (i = 0; i < 10; i++) {
pthread_spin_lock(&g_spinlock);
g_count++;
printf("自旋线程%ld,计数:%d\n", pthread_self(), g_count);
pthread_spin_unlock(&g_spinlock);
usleep(100);
}
return NULL;
}

int main() {
pthread_spin_init(&g_spinlock, 0);
pthread_t t1, t2;
pthread_create(&t1, NULL, spin_thread, NULL);
pthread_create(&t2, NULL, spin_thread, NULL);

pthread_join(t1, NULL);
pthread_join(t2, NULL);

pthread_spin_destroy(&g_spinlock);
return 0;
}

条件变量配套锁

Linux条件变量需与pthread_mutex_t配合使用,与C++11条件变量和mutex的配合逻辑一致,线程调用wait等待条件时,会自动释放持有的锁,被唤醒后重新竞争锁,降低死锁风险,可实现线程间等待-通知同步,常用于生产者-消费者模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t g_cond_mutex;
pthread_cond_t g_cond;
int g_flag = 0;

// 等待条件满足的线程
void* wait_thread(void* arg) {
pthread_mutex_lock(&g_cond_mutex);
printf("等待线程%ld进入等待状态\n", pthread_self());
while (g_flag == 0) {
pthread_cond_wait(&g_cond, &g_cond_mutex);
}
printf("等待线程%ld被唤醒,开始执行\n", pthread_self());
pthread_mutex_unlock(&g_cond_mutex);
return NULL;
}

// 触发条件的线程
void* signal_thread(void* arg) {
sleep(2);
pthread_mutex_lock(&g_cond_mutex);
g_flag = 1;
printf("唤醒线程%ld触发条件\n", pthread_self());
pthread_cond_signal(&g_cond);
pthread_mutex_unlock(&g_cond_mutex);
return NULL;
}

int main() {
pthread_mutex_init(&g_cond_mutex, NULL);
pthread_cond_init(&g_cond, NULL);

pthread_t t_wait, t_signal;
pthread_create(&t_wait, NULL, wait_thread, NULL);
pthread_create(&t_signal, NULL, signal_thread, NULL);

pthread_join(t_wait, NULL);
pthread_join(t_signal, NULL);

pthread_mutex_destroy(&g_cond_mutex);
pthread_cond_destroy(&g_cond);
return 0;
}

编译说明:Linux下编译pthread代码需添加-pthread参数,示例命令:gcc 文件名.c -o 可执行文件 -pthread。

锁类型对应关系

C++11标准锁 Linux原生锁 特性说明
std::mutex pthread_mutex_t(默认属性) 独占、非递归、阻塞加锁
std::recursive_mutex pthread_mutex_t(递归属性) 独占、可重入、阻塞加锁
std::shared_mutex pthread_rwlock_t 读共享、写互斥
无直接对应 pthread_spinlock_t 自旋、无阻塞、用户态循环

锁的使用建议

  • 控制锁的作用范围,遵循最小粒度原则,提升并发效率;
  • 避免在临界区内执行耗时操作,减少锁占用时长;
  • 优先选用RAII锁管理器,减少手动解锁操作带来的失误;
  • Linux底层开发可选用自旋锁、读写锁,上层应用开发可优先使用C++11标准锁,平衡兼容性与易用性。

RAII锁管理器与条件变量

手动调用lock()与unlock()操作锁资源,存在漏解锁、重复解锁、异常跳转未解锁等风险,易引发死锁、资源泄露等问题。C++11依托RAII资源管理思想,推出标准化锁管理器,可自动完成锁资源的申请与释放,降低人为操作失误;同时搭配条件变量,实现线程间等待-通知的高级同步功能,二者结合适用于工业级多线程开发场景。

RAII锁管理器

RAII是C++资源管理的核心范式,核心逻辑是将资源生命周期与对象生命周期绑定,利用编译器自动调用对象析构函数的特性,实现资源的自动管理。针对线程互斥锁,C++11提供两款核心RAII锁管理器,分别适配常规场景与复杂场景,底层均依托互斥锁实现,且遵守锁的对称性规则。

std::lock_guard

std::lock_guard是轻量化的RAII锁管理器,属于模板类,可适配各类互斥锁,设计偏向简洁稳定,无需手动加锁、解锁,依靠对象作用域自动管理锁生命周期,适用于常规多线程同步场景。

std::lock_guard简化实现

以下为剔除冗余封装后的简化实现代码,保留核心逻辑,可辅助理解底层设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <mutex>

template <typename MutexType>
class lock_guard {
public:
explicit lock_guard(MutexType& mtx) : mutex_ref(mtx) {
mutex_ref.lock();
}

~lock_guard() {
mutex_ref.unlock();
}

lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;

lock_guard(lock_guard&&) = delete;
lock_guard& operator=(lock_guard&&) = delete;

private:
MutexType& mutex_ref;
};

实现逻辑说明

  1. 模板类设计:适配不同类型的互斥锁,提升通用性;
  2. 构造函数:接收锁引用,构造时自动执行加锁操作;
  3. 析构函数:对象离开作用域时自动调用,完成解锁操作,无论代码正常执行或触发异常,均可保证解锁流程执行;
  4. 禁止拷贝与移动:避免多个对象管理同一把锁,降低死锁风险;
  5. 私有引用成员:存储锁的引用,保障多线程共享同一把锁。

特性与适用场景

  • 资源开销低,无额外内存占用,运行效率与手动加锁接近;
  • 锁生命周期与对象作用域绑定,作用域结束立即释放锁;
  • 无手动操作接口,全程自动化,适用于单一临界区、无复杂等待逻辑的场景。

实战代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

mutex mtx;

void safePrint(char ch) {
lock_guard<mutex> lock(mtx);
for (int i = 0; i < 10; ++i) {
cout << ch;
}
cout << endl;
}

int main() {
thread t1(safePrint, 'A');
thread t2(safePrint, 'B');
t1.join();
t2.join();
return 0;
}

std::unique_lock

std::unique_lock是lock_guard的扩展版本,同样遵循RAII机制,保留自动解锁功能,同时新增多项适配复杂场景的功能,核心差异与特性如下:

  • 支持手动解锁与延迟加锁,可灵活控制锁的生命周期,无需严格绑定作用域;
  • 支持移动语义,可转移锁的所有权,适配函数间传递锁资源的场景;
  • 支持超时加锁,搭配定时锁可避免线程永久阻塞;
  • 资源开销略高于lock_guard,常规场景优先选用lock_guard,复杂场景可选用unique_lock。

条件变量

互斥锁主要解决线程互斥访问共享资源的问题,无法实现线程间协同等待,例如生产者-消费者类场景,单纯依靠互斥锁会出现无效循环等待,占用CPU资源。条件变量是针对此类场景设计的同步工具,需与互斥锁配合使用,实现线程的阻塞等待与主动唤醒,是构建生产者-消费者模型、线程池的核心组件。

条件变量原理

条件变量的核心逻辑为原子性释放锁与阻塞等待,规避死锁风险,底层流程分为两步:

  1. 等待流程:线程调用wait()方法时,原子性释放持有的互斥锁,随后进入阻塞态,释放CPU资源,其他线程可正常竞争锁资源;
  2. 唤醒流程:共享资源满足条件后,唤醒线程调用notify_one()或notify_all()发送通知,阻塞线程被唤醒后,重新竞争互斥锁,获取锁后继续执行后续逻辑。

使用提示:条件变量需搭配互斥锁使用,等待条件建议通过while循环判断,而非if语句,可规避虚假唤醒问题,保障条件判断的严谨性。

条件变量常用API

  • std::condition_variable:C++11标准条件变量,仅支持搭配std::unique_lock使用;

  • wait(unique_lock& lock, Predicate pred):阻塞等待,传入锁与条件判断逻辑,条件不满足时释放锁并阻塞;

  • notify_one():唤醒一个正在等待的线程,适合单生产者单消费者场景。

  • notify_all():唤醒所有正在等待的线程,适合多生产者多消费者场景。

条件变量与互斥锁配合(生产者 - 消费者模型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
using namespace std;

// 共享队列
queue<int> msg_queue;
// 互斥锁
mutex mtx;
// 条件变量
condition_variable cond;
// 队列最大容量
const int MAX_SIZE = 5;

// 生产者线程
void producer() {
for (int i = 1; i <= 10; ++i) {
unique_lock<mutex> lock(mtx);
// 队列满时,生产者等待
while (msg_queue.size() >= MAX_SIZE) {
cout << "队列已满,生产者等待..." << endl;
cond.wait(lock);
}
msg_queue.push(i);
cout << "生产者生产数据:" << i << ",队列当前大小:" << msg_queue.size() << endl;
cond.notify_one();
}
}

// 消费者线程
void consumer() {
for (int i = 1; i <= 10; ++i) {
unique_lock<mutex> lock(mtx);
// 队列空时,消费者等待
while (msg_queue.empty()) {
cout << "队列已空,消费者等待..." << endl;
cond.wait(lock);
}
int data = msg_queue.front();
msg_queue.pop();
cout << "消费者消费数据:" << data << ",队列当前大小:" << msg_queue.size() << endl;
cond.notify_one();
}
}

int main() {
thread t_producer(producer);
thread t_consumer(consumer);
t_producer.join();
t_consumer.join();
return 0;
}

条件变量与互斥锁区别

互斥锁用于控制线程对共享资源的互斥访问,限定同一时间仅有单个线程操作共享资源,线程的调度与唤醒由系统完成。条件变量用于实现线程间的协同等待与通知,可指定线程的唤醒时机,减少线程无效循环的资源消耗,提升处理器使用效率。条件变量需与互斥锁配合使用,线程执行wait()操作时会释放所持有的锁资源,被唤醒后重新竞争锁,以此避免死锁。条件变量提供wait()、wait_for()用于等待条件满足,notify_one()、notify_all()用于唤醒等待状态的线程,可用于构建生产者-消费者模型等多线程同步场景。

总结

C++11线程类不支持拷贝操作,仅支持资源移动,该设计可避免多对象管理同一内核资源引发的程序异常。线程运行过程中需要通过join()完成资源回收,未执行该操作会造成内核资源无法释放,进而引发内存泄漏与程序异常。线程函数支持值传递、引用传递与右值引用三种参数传递形式,引用传递需要通过std::ref完成参数包装,否则无法通过编译。
多线程访问共享资源时,需要使用互斥锁进行保护,锁的加锁与解锁操作需要保持对称,且锁对象需要在多个线程间共享。RAII锁管理器可自动完成锁的申请与释放,降低手动操作产生的异常风险。C++11多线程库对平台原生API进行了封装,简化了线程开发流程,提升了跨平台兼容性。

Windows与Linux平台下,线程对象移动后的资源处理逻辑存在差异,程序开发需要适配不同平台的运行特性。Linux平台提供了多样化的线程同步机制,了解底层线程原理能够加深对C++11线程库的理解。线程开发需要从基础创建与参数传递逐步延伸至同步机制与条件变量的应用,理解线程状态转换、内核栈与资源转移的底层逻辑,能够支撑程序的设计与问题排查。

线程开发过程中存在多项需要注意的问题,未调用join()会导致资源泄漏;互斥锁定义为局部变量无法实现线程间的互斥效果;引用传递未使用std::ref会触发编译错误;移动后的线程对象执行操作会引发运行时错误;锁的加解锁操作不对称会引发资源泄漏或死锁,上述问题均会影响程序的稳定运行。