目录
[1.1 定义](#1.1 定义)
[线程同步(Thread Synchronization)](#线程同步(Thread Synchronization))
[线程异步(Asynchronous Thread Operations)](#线程异步(Asynchronous Thread Operations))
[1.2 关键区别总结](#1.2 关键区别总结)
[2.1 互斥锁(Mutex)](#2.1 互斥锁(Mutex))
[2.1.1 定义](#2.1.1 定义)
[2.1.2 核心用途](#2.1.2 核心用途)
[2.1.3 工作原理](#2.1.3 工作原理)
[2.1.4 实现方式](#2.1.4 实现方式)
[2.1.5 关键注意事项](#2.1.5 关键注意事项)
[2.1.6 总结](#2.1.6 总结)
[2.2 条件变量(std::condition_variable)](#2.2 条件变量(std::condition_variable))
[2.2.1 核心作用](#2.2.1 核心作用)
[2.2.2 关键接口与行为](#2.2.2 关键接口与行为)
[2.2.3 典型使用流程](#2.2.3 典型使用流程)
[2.2.4 注意事项](#2.2.4 注意事项)
[2.2.5 底层原理](#2.2.5 底层原理)
[2.3 信号量(Semaphore)](#2.3 信号量(Semaphore))
[2.3.1 限制并发访问数量](#2.3.1 限制并发访问数量)
[2.3.2 资源管理](#2.3.2 资源管理)
[2.3.3 线程间的事件通知](#2.3.3 线程间的事件通知)
[2.4 原子操作(Atomic Operation)](#2.4 原子操作(Atomic Operation))
[2.4.1 C++原子操作](#2.4.1 C++原子操作)
[2.4.2 C++互斥锁](#2.4.2 C++互斥锁)
[2.4.3 对比总结](#2.4.3 对比总结)
[2.5 屏障(std::barrier / std::latch)](#2.5 屏障(std::barrier / std::latch))
[2.5.1 std::latch(单次屏障)](#2.5.1 std::latch(单次屏障))
[2.5.2 std::barrier(可复用屏障)](#2.5.2 std::barrier(可复用屏障))
[2.5.3 关键区别总结](#2.5.3 关键区别总结)
一、线程同步与线程异步
在C++中,线程同步和线程异步是处理多线程任务协作的两种核心机制。
1.1 定义
线程同步(Thread Synchronization)
定义:线程同步通过协调多个线程的执行顺序或资源访问,确保数据一致性和避免竞争条件(Race Condition)。同步机制会强制线程按特定规则执行,可能引发阻塞。
线程异步(Asynchronous Thread Operations)
定义 :线程异步指主线程无需等待子线程完成即可继续执行,子线程通过回调、事件或返回值(如std::future)通知结果。异步操作通常用于提升并发性能。
1.2 关键区别总结
| 特性 | 线程同步 | 线程异步 |
|---|---|---|
| 目的 | 协调线程执行顺序/资源访问 | 提升并发性能,避免主线程阻塞 |
| 阻塞行为 | 可能阻塞(如等待锁、条件变量) | 非阻塞(主线程继续执行) |
| 结果获取 | 直接访问共享数据或通过同步机制 | 通过回调、std::future或事件 |
| 复杂度 | 较高(需处理死锁、活锁等问题) | 较高(需管理任务生命周期) |
补充说明
- 同步的代价:过度使用同步(如粗粒度锁)可能导致性能下降,需尽量缩小临界区范围。
- 异步的挑战:需处理任务取消、异常传播等问题,C++20的协程(Coroutines)可简化异步代码。
- 工具选择 :
- 同步:
std::mutex、std::lock_guard、std::condition_variable。 - 异步:
std::async、std::future、线程池(如Intel TBB、Boost.Asio)。
- 同步:
通过合理结合同步与异步机制,可以构建高效且正确的多线程程序。
二、同步机制
2.1 互斥锁(Mutex)
C++互斥锁是一种用于多线程同步的核心机制,确保同一时刻只有一个线程能访问共享资源,避免数据竞争。
2.1.1 定义
互斥锁(Mutex,全称Mutual Exclusion)通过"锁定-解锁"机制实现线程同步,确保共享资源(如全局变量、文件、内存等)的互斥访问
2.1.2 核心用途
- 保护共享资源:防止多个线程同时读写导致数据损坏。
- 线程同步:控制操作顺序(如初始化、状态修改)。
- 避免数据竞争:解决多线程无序访问同一数据的不可预测行为。
2.1.3 工作原理
锁定(Lock)
- 线程尝试获取锁时,若锁未被占用,则成功获取并进入临界区;否则阻塞等待。
- 示例:
mtx.lock()(std::mutex)。
访问共享资源
- 持有锁的线程安全操作共享数据。
解锁(Unlock)
- 线程释放锁,唤醒等待队列中的其他线程。
- 示例:
mtx.unlock()。
2.1.4 实现方式
(1)基础锁:std::mutex
- 手动管理 :需显式调用
lock()和unlock(),易因异常或遗漏导致死锁。 - 示例:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
mtx.lock();
++counter;
mtx.unlock();
}
int main() {
std::thread t1(increment), t2(increment);
t1.join(); t2.join();
std::cout << "Counter: " << counter << std::endl; // 输出2
}
(2)RAII封装:std::lock_guard
- 自动管理:构造时加锁,离开作用域(析构时)解锁,避免忘记释放。
- 示例:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void safe_increment(int& counter) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
} // 自动解锁
int main() {
int counter = 0;
std::thread t1(safe_increment, std::ref(counter)),
t2(safe_increment, std::ref(counter));
t1.join(); t2.join();
std::cout << "Counter: " << counter << std::endl; // 输出2
}
(3)高级锁:std::unique_lock
- 灵活性:支持延迟锁定、手动解锁、条件变量配合。
- 示例:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // 等待条件满足
std::cout << "Worker processed data." << std::endl;
}
int main() {
std::thread t(worker);
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 唤醒线程
t.join();
}
-
**
std::unique_lock**比 lock_guard 强在哪-
控制权更灵活 :std::lock_guard 构造时就必须加锁,析构时才解锁,中间无法干预;std::unique_lock 允许在作用域内手动调用
unlock()提前释放锁,用完再lock()重新加锁,适合临界区中间夹杂耗时操作的场景 。 -
支持锁所有权转移:std::unique_lock 对象不可复制但支持移动(std::move),可以将锁的管理权从一个对象转移到另一个对象,而 std::lock_guard 既不可复制也不可移动 。
-
条件变量必选搭档:在使用 std::condition_variable 进行线程等待或通知时,必须搭配 std::unique_lock,std::lock_guard 无法配合使用,这是实现生产者 - 消费者模型的基础 。
-
2.1.5 关键注意事项
(1)死锁预防
- 避免嵌套锁 :确保加锁顺序一致,或使用
std::lock同时锁定多个互斥量。 - 示例:
cpp
std::mutex mtx1, mtx2;
std::lock(mtx1, mtx2); // 原子化锁定,避免死锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
(2)性能优化
- 缩小临界区:仅保护必要代码,减少锁持有时间。
- 读写锁 :读多写少场景使用
std::shared_mutex提升并发性。
(3)异常安全
- 优先使用
std::lock_guard或std::unique_lock,确保异常时自动释放锁。
2.1.6 总结
- 选择工具 :
- 简单场景:
std::mutex+std::lock_guard。 - 复杂同步:
std::unique_lock+ 条件变量。
- 简单场景:
- 核心原则 :最小化锁粒度、避免死锁、确保异常安全。
通过合理使用互斥锁,可以高效解决多线程数据竞争问题,同时平衡性能与安全性。
2.2 条件变量(std::condition_variable)
C++条件变量(std::condition_variable)是多线程同步的核心工具,用于实现线程间的高效通信和条件等待。
2.2.1 核心作用
条件变量与互斥锁(std::unique_lock)配合使用,允许线程在满足特定条件前主动阻塞,避免忙等待(busy-waiting),提升性能。典型场景包括:
- 生产者-消费者模型:消费者等待生产者填充数据。
- 任务调度:工作线程等待任务队列非空。
- 资源同步:线程等待共享资源可用。
2.2.2 关键接口与行为
(1)等待函数
wait(unique_lock<mutex>& lock)- 原子操作:释放锁 → 阻塞线程 → 被唤醒后重新加锁。
- 虚假唤醒:可能被无理由唤醒,需结合谓词(Predicate)检查条件。
wait(unique_lock<mutex>& lock, Predicate pred)- 仅当
pred()返回false时阻塞,唤醒后重新检查pred(),避免虚假唤醒。
- 仅当
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return data_ready; }); // 谓词检查
std::cout << "Data processed!\n";
}
int main() {
std::thread t(worker);
{
std::lock_guard<std::mutex> lock(mtx);
data_ready = true;
}
cv.notify_one();
t.join();
return 0;
}
wait_for()/wait_until()- 支持超时等待,返回
cv_status::timeout或cv_status::no_timeout。
- 支持超时等待,返回
(2)通知函数
notify_one()
唤醒一个等待线程(具体哪个不确定)。notify_all()
唤醒所有等待线程,适用于多消费者场景。
2.2.3 典型使用流程
- 等待线程 :
- 获取
std::unique_lock<std::mutex>。 - 调用
cv.wait(lock, predicate)检查条件。
- 获取
- 通知线程 :
- 修改共享变量(需持有锁)。
- 调用
cv.notify_one()或cv.notify_all()。
示例:生产者-消费者模型
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> task_queue;
bool stop_flag = false;
void producer() {
for (int i = 0; i < 5; ++i) {
std::lock_guard<std::mutex> lock(mtx);
task_queue.push(i);
std::cout << "Produced: " << i << "\n";
cv.notify_one();
}
{
std::lock_guard<std::mutex> lock(mtx);
stop_flag = true;
cv.notify_all();
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !task_queue.empty() || stop_flag; });
if (stop_flag && task_queue.empty()) break;
int task = task_queue.front();
task_queue.pop();
std::cout << "Consumed: " << task << "\n";
}
}
int main() {
std::thread p(producer);
std::thread c(consumer);
p.join();
c.join();
return 0;
}
2.2.4 注意事项
- 必须与
std::unique_lock配合 :std::condition_variable仅支持unique_lock,而非lock_guard。 - 避免唤醒丢失:确保通知在等待之后发出(通常通过锁保护共享状态)。
- 性能权衡 :
notify_all()可能引发大量线程竞争,需根据场景选择。 - 通用版本 :
std::condition_variable_any支持任意锁类型,但性能较低。
2.2.5 底层原理
- 线程阻塞与唤醒 :依赖操作系统提供的条件变量机制(如 Linux 的
futex)。 - 原子性保证 :
wait()的锁释放与线程阻塞是原子的,防止竞态条件。
通过合理使用条件变量,可以高效实现线程同步,避免资源浪费和死锁风险。
2.3 信号量(Semaphore)
在C++多线程编程中,信号量(Semaphore) 是一种非常重要的同步原机制。主要支持限制并发数和资源管理。当然也支持线程间事件通知。
C++对信号量的支持主要分为两个阶段:
-
C++20 及以后:标准库直接提供了 <semaphore> 头文件,包含标准的信号量类。
-
C++20 以前:没有直接的信号量类,需要使用 std::mutex 和 std::condition_variable 手动封装。
这边我讲C++20以前的写法,C++20以后更简单触类旁通。
在 C++11/14/17 中,由于没有标准的 <semaphore>,通常的做法是结合 互斥锁 (std::mutex) 和 条件变量 (std::condition_variable) 来实现。
2.3.1 限制并发访问数量
由于 C++11 未引入 std::semaphore,开发者通常通过以下方式模拟信号量:
- 互斥锁:std::unique_lock 保护临界区。
- 原子计数器 :使用
std::atomic<int>记录资源数量。 - 条件变量 :通过
std::condition_variable实现线程阻塞与唤醒。
适用于线程池限流或同步的辅助工具。
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
class Semaphore {
private:
std::mutex mtx;
std::condition_variable cv;
std::atomic<int> count;
public:
Semaphore(int initial_count) : count(initial_count) {}
void acquire() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]() { return count > 0; });
--count;
}
void release() {
std::unique_lock<std::mutex> lock(mtx);
++count;
cv.notify_one();
}
};
// 使用示例:限制并发数
Semaphore pool(3); // 最大并发数 3
void worker(int id) {
pool.acquire();
std::cout << "Thread " << id << " is working.\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread " << id << " finished.\n";
pool.release();
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 6; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
输出:
cpp
Thread 0 is working.
Thread 1 is working.
Thread 2 is working.
Thread 0 finished.
Thread 3 is working. // 线程 3 等待线程 0 释放许可后执行
...
2.3.2 资源管理
协调生产者和消费者的速度,避免缓冲区溢出或下溢。
- 使用两个信号量:
empty_slots(空位数量)和filled_slots(数据项数量)。 - 生产者等待
empty_slots,消费者等待filled_slots。
示例代码(生产者-消费者模型,C++11 实现):
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
class Semaphore {
private:
std::mutex mtx;
std::condition_variable cv;
int count;
public:
Semaphore(int initial_count) : count(initial_count) {}
void acquire() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]() { return count > 0; });
--count;
}
void release() {
std::unique_lock<std::mutex> lock(mtx);
++count;
cv.notify_one();
}
};
std::queue<int> buffer;
const int buffer_size = 3;
Semaphore empty_slots(buffer_size);// 空位信号量
Semaphore filled_slots(0); // 数据信号量
void producer() {
for (int i = 0; i < 5; ++i) {
empty_slots.acquire(); // 等待空位
buffer.push(i);
std::cout << "Produced: " << i << "\n";
filled_slots.release(); // 通知消费者
}
}
void consumer() {
for (int i = 0; i < 5; ++i) {
filled_slots.acquire(); // 等待数据
int item = buffer.front();
buffer.pop();
std::cout << "Consumed: " << item << "\n";
empty_slots.release(); // 通知生产者
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
输出:
cpp
Produced: 0
Consumed: 0
Produced: 1
Consumed: 1
... // 严格交替执行,缓冲区大小限制为 3
2.3.3 线程间的事件通知
- 场景:一个线程等待另一个线程完成特定任务后再继续执行。
- 示例 :
- 线程A等待线程B初始化完成后才开始工作。
cpp
#include <iostream>
#include <thread>
#include <semaphore>
std::binary_semaphore ready(0); // 二进制信号量(初始为0)
void worker() {
std::cout << "Worker initializing...\n";
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Worker ready!\n";
ready.release(); // 通知主线程
}
int main() {
std::thread t(worker);
ready.acquire(); // 等待worker就绪
std::cout << "Main thread continues after worker is ready!\n";
t.join();
return 0;
}
2.4 原子操作(Atomic Operation)
讲原子操作那就不得不跟锁比较一番。
2.4.1 C++原子操作
核心特性
- 不可分割性:操作要么完全执行,要么完全不执行,不存在中间状态。
- 内存可见性:一个线程对原子变量的修改能被其他线程及时看到。
- 指令有序性:通过内存序控制编译器和CPU的指令重排,保证操作执行顺序符合预期。
主要应用场景
- 简单的变量读写、累加、状态切换:例如计数器、标志位更新等。
- 无锁数据结构:如无锁队列、无锁栈等。
优势
- 高性能:原子操作通常在用户态完成,无内核切换开销,延迟远低于互斥锁。
- 无死锁风险:不涉及锁的获取和释放,避免了死锁问题。
- 可控性强:允许精细控制内存对齐、CPU缓存等底层细节,适合对延迟要求极高的场景(如自动驾驶)。
劣势
- 功能受限:仅保护单个变量的单次操作,无法保护复杂的临界区代码块。
- 调试难度大:内存序、ABA问题等导致的bug极难调试。
cpp
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 原子自增
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl; // 输出: 200000
return 0;
}
2.4.2 C++互斥锁
核心特性
- 独占访问:同一时间只有一个线程能进入临界区,保证操作的独占性。
- 多种锁类型 :包括基本互斥锁(
std::mutex)、递归锁(std::recursive_mutex)、带超时的锁(std::timed_mutex)和读写锁(std::shared_mutex)等。
主要应用场景
- 复杂的多步操作:如修改多个关联变量、操作复杂结构体等。
- 资源访问控制:需要保护任意复杂的临界区代码块时。
优势
- 功能全面:可保护任意复杂的临界区代码块,支持多种锁类型以满足不同需求。
- 调试相对简单:临界区逻辑清晰,调试难度低于原子操作。
劣势
- 性能开销大:加锁/解锁会触发内核态上下文切换,高并发下锁竞争会导致性能急剧下降。
- 死锁风险:多锁场景下存在死锁风险,需谨慎设计加锁顺序。
cpp
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mtx;
int shared_data = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁和解锁
++shared_data;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final shared_data value: " << shared_data << std::endl; // 输出: 200000
return 0;
}
2.4.3 对比总结
| 特性 | 原子操作 | 互斥锁 |
|---|---|---|
| 执行模式 | 用户态完成,无线程阻塞 | 内核态同步,线程阻塞会触发上下文切换 |
| 保护范围 | 仅保护单个变量的单次操作 | 可保护任意复杂的临界区代码块 |
| 死锁风险 | 无死锁风险 | 多锁场景下有死锁风险 |
| 性能表现 | 低延迟,高并发下性能稳定 | 高并发下锁竞争激烈,性能急剧下降 |
| 调试难度 | 内存序、ABA问题导致的bug极难调试 | 临界区逻辑清晰,调试相对简单 |
| 适用场景 | 简单变量操作、无 |
2.5 屏障(std::barrier / std::latch)
std::latch和std::barrier都是C++20引入的线程同步工具,但它们的用途和行为有显著区别。
2.5.1 std::latch(单次屏障)
- 用途:用于同步多个线程到某个单次执行的点(计数器归零后不可复用)。
- 特点 :
- 初始化时设置计数器值,线程通过
count_down()减少计数器。 - 提供
wait()和arrive_and_wait()阻塞线程,直到计数器归零。 - 不可复用:计数器归零后无法重置或增加。
- 适用于一次性任务(如初始化阶段、任务完成后的清理)。
- 初始化时设置计数器值,线程通过
- 示例代码:
cpp
#include <iostream>
#include <latch>
#include <thread>
#include <vector>
int main() {
const int N = 3;
std::latch latch(N); // 初始化计数器为3
auto worker = [&latch](int id) {
std::cout << "Worker " << id << " started\n";
latch.count_down(); // 减少计数器
std::cout << "Worker " << id << " finished\n";
};
std::vector<std::thread> threads;
for (int i = 0; i < N; ++i) {
threads.emplace_back(worker, i);
}
latch.wait(); // 阻塞主线程直到计数器归零
std::cout << "All workers completed\n";
for (auto& t : threads) {
t.join();
}
return 0;
}
输出:
cpp
Worker 0 started
Worker 1 started
Worker 2 started
Worker 2 finished
Worker 0 finished
Worker 1 finished
All workers completed
2.5.2 std::barrier(可复用屏障)
- 用途:用于同步多个线程到周期性执行的点(计数器可重置)。
- 特点 :
- 初始化时设置计数器值,线程通过
arrive_and_wait()减少计数器并阻塞。 - 可复用:计数器归零后自动重置(或通过回调函数重置)。
- 适用于循环任务(如迭代计算、帧渲染)。
- 初始化时设置计数器值,线程通过
- 示例代码:
cpp
#include <iostream>
#include <barrier>
#include <thread>
#include <vector>
int main() {
const int N = 3;
const int iterations = 2;
std::barrier barrier(N); // 初始化计数器为3
auto worker = [&barrier](int id) {
for (int i = 0; i < iterations; ++i) {
std::cout << "Worker " << id << " iteration " << i << "\n";
barrier.arrive_and_wait(); // 等待所有线程到达
}
};
std::vector<std::thread> threads;
for (int i = 0; i < N; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
输出:
cpp
Worker 0 iteration 0
Worker 1 iteration 0
Worker 2 iteration 0
Worker 0 iteration 1
Worker 1 iteration 1
Worker 2 iteration 1
2.5.3 关键区别总结
| 特性 | std::latch |
std::barrier |
|---|---|---|
| 复用性 | 单次使用(计数器不可重置) | 可复用(计数器自动或通过回调重置) |
| 主要方法 | count_down(), wait() |
arrive_and_wait() |
| 典型场景 | 一次性任务(如初始化、清理) | 周期性任务(如迭代计算) |
| 初始化 | std::latch(N) |
std::barrier(N) |
何时选择?
- 用
std::latch:当需要同步线程到一个固定点(如所有线程完成初始化后继续执行)。 - 用
std::barrier:当需要同步线程到多个周期性点(如每轮迭代后等待其他线程)。
通过合理选择这两个工具,可以高效实现复杂的线程同步逻辑。