C++多线程编程
C++ 多线程编程从入门到进阶 - 完整学习笔记
目录
- 线程的基本概念
- 线程基本操作
- 线程函数传C++类实例指针惯用法
- 整型变量的原子操作
- Linux线程同步对象
- Windows线程资源同步对象
- C++11/14/17线程同步对象
- 如何确保创建的线程一定运行起来
- 多线程使用锁实践经验总结
- 线程局部存储
- C++标准库容器的线程安全性
- C库的非线程安全函数
- 线程池与队列系统的设计
- 纤程与协程
- C++异步编程:async/future/promise
- std::call_once与线程安全单例
- 内存序(memory_order)详解
- 双重检查锁定(DCLP)
- 数据竞争 vs 竞争条件
- std::shared_ptr的线程安全性
- 无锁编程简介
- 常见多线程设计模式
- 本章总结
1. 线程的基本概念
1.1 什么是线程?
线程(Thread) 是进程中实际执行代码的最小单元,由操作系统调度管理。
💡 通俗理解:如果把进程比作一个工厂,那线程就是工厂里的工人。工厂提供环境和资源,工人负责实际干活。
1.2 进程与线程的关系
| 特性 | 进程 | 线程 |
|---|---|---|
| 定义 | 程序运行的实例 | 进程中的执行单元 |
| 资源 | 拥有独立的地址空间 | 共享进程的资源 |
| 通信 | 需要IPC机制 | 可直接共享内存 |
| 开销 | 创建/切换开销大 | 创建/切换开销小 |
1.3 核心要点
📌 一个进程至少有一个线程
这个线程称为主线程。线程是进程中实际干活的单位,所以进程必须至少有一个线程。
📌 主线程退出,支线程会怎样?
| 操作系统 | 行为 |
|---|---|
| Windows | 主线程退出 → 整个进程结束 → 所有支线程强制终止 |
| Linux | 主线程退出 → 支线程可能继续运行 → 但进程变成僵尸进程(defunct) |
⚠️ 注意:实际开发中应避免产生僵尸进程,主线程应该等待工作线程完成后再退出。
僵尸进程示例:
# 查看僵尸进程(带有 <defunct> 字样)
[root@localhost ~]# ps -ef
root 60928 1 0 14:48 pts/1 00:00:00 [linuxtid] <defunct>
📌 某个线程崩溃,会导致进程退出吗?
答案:通常会的!
- 每个线程都是独立执行的,有自己的上下文堆栈
- 但线程崩溃(如 Segment Fault)会产生信号
- 操作系统默认处理方式是结束整个进程
- 结果:进程中所有其他线程也随之消失
2. 线程基本操作
2.1 创建线程
Linux (pthread)
#include <pthread.h>
void* thread_func(void* arg) {
// 线程执行代码
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, NULL); // 等待线程结束
return 0;
}
C++11 (std::thread)
#include <thread>
void thread_func() {
// 线程执行代码
}
int main() {
std::thread t(thread_func);
t.join(); // 等待线程结束
return 0;
}
2.2 线程操作对比
| 操作 | Linux (pthread) | C++11 (std::thread) |
|---|---|---|
| 创建 | pthread_create() |
std::thread 构造函数 |
| 等待 | pthread_join() |
join() |
| 分离 | pthread_detach() |
detach() |
| 获取ID | pthread_self() |
std::this_thread::get_id() |
| 休眠 | sleep() |
std::this_thread::sleep_for() |
3. 线程函数传C++类实例指针惯用法
3.1 问题场景
当需要在线程中操作某个类的成员时,如何传递类实例?
3.2 解决方案
class Worker {
public:
void doWork() {
// 工作代码
}
};
// 线程函数
void* thread_func(void* arg) {
Worker* worker = static_cast<Worker*>(arg);
worker->doWork();
return NULL;
}
int main() {
Worker worker;
pthread_t tid;
// 传递 this 指针
pthread_create(&tid, NULL, thread_func, &worker);
pthread_join(tid, NULL);
return 0;
}
3.3 使用 C++11 更优雅的方式
class Worker {
public:
void start() {
thread_ = std::thread(&Worker::doWork, this);
}
void join() {
if (thread_.joinable()) {
thread_.join();
}
}
private:
void doWork() {
// 工作代码
}
std::thread thread_;
};
💡 核心思想:将类实例的指针(通常是
this)作为参数传递给线程函数,线程函数再通过该指针调用类的成员方法。
4. 整型变量的原子操作
4.1 为什么整型操作不是原子的?
🔍 一个真实的故事
作者曾开发即时通讯软件,多线程操作用户ID时没加锁,结果老板发给媳妇的消息被发到了作者账号上...从此老板看作者的眼神都怪怪的。
📊 a++ 的汇编指令分析
int a = 0;
a++;
看似一条语句,实际对应三条汇编指令:
mov eax, dword ptr [a] ; ① 从内存读取 a 到寄存器
inc eax ; ② 寄存器值加 1
mov dword ptr [a], eax ; ③ 将结果写回内存
⚠️ 竞态条件示例
初始值: a = 0
线程1执行: ① ② (eax = 1)
↓ 此时被切换
线程2执行: ① ② ③ (a = 1)
↓ 切回线程1
线程1执行: ③ (a = 1)
最终结果: a = 1 (期望是 2)
4.2 Windows 原子操作 API
#include <Windows.h>
LONG value = 0;
// 自增1,返回增加后的值
InterlockedIncrement(&value);
// 自减1
InterlockedDecrement(&value);
// 增加指定值
InterlockedExchangeAdd(&value, 10);
// 比较并交换
InterlockedCompareExchange(&value, newValue, expectedValue);
4.3 C++11 原子操作(跨平台推荐)
#include <atomic>
std::atomic<int> value(0);
// 基本操作
value = 10; // 存储
int v = value; // 读取
value++; // 原子自增
value--; // 原子自减
value += 5; // 原子加
// 显式方法
value.store(10); // 存储值
v = value.load(); // 读取值
v = value.exchange(20); // 交换并返回旧值
// 比较并交换 (CAS)
int expected = 10;
bool success = value.compare_exchange_strong(expected, 20);
4.4 常用原子类型
| 类型别名 | 定义 |
|---|---|
std::atomic_bool |
std::atomic<bool> |
std::atomic_int |
std::atomic<int> |
std::atomic_long |
std::atomic<long> |
std::atomic_int64_t |
std::atomic<int64_t> |
⚠️ 注意:
std::atomic的拷贝构造函数被禁用,以下写法在 Linux 会编译失败:std::atomic<int> value = 99; // ❌ 错误 std::atomic<int> value; // ✓ 正确 value = 99;
5. Linux线程同步对象
5.1 互斥体 (Mutex)
初始化方式
方式一:静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
方式二:动态初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 使用完毕后销毁
pthread_mutex_destroy(&mutex);
基本使用
pthread_mutex_lock(&mutex);
// 临界区代码
pthread_mutex_unlock(&mutex);
互斥体类型
| 类型 | 说明 |
|---|---|
PTHREAD_MUTEX_NORMAL |
普通锁(默认),同线程重复加锁会死锁 |
PTHREAD_MUTEX_ERRORCHECK |
检错锁,同线程重复加锁返回 EDEADLK |
PTHREAD_MUTEX_RECURSIVE |
递归锁,同线程可重复加锁,需相同次数解锁 |
设置互斥体类型
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
5.2 条件变量 (Condition Variable)
条件变量用于线程间的等待/通知机制。
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
bool ready = false;
// 等待线程
void* wait_thread(void* arg) {
pthread_mutex_lock(&mutex);
while (!ready) {
// 等待条件满足(自动释放锁,唤醒时重新获取锁)
pthread_cond_wait(&cond, &mutex);
}
// 条件满足,执行操作
pthread_mutex_unlock(&mutex);
return NULL;
}
// 通知线程
void* signal_thread(void* arg) {
pthread_mutex_lock(&mutex);
ready = true;
pthread_cond_signal(&cond); // 唤醒一个等待线程
// pthread_cond_broadcast(&cond); // 唤醒所有等待线程
pthread_mutex_unlock(&mutex);
return NULL;
}
5.3 信号量 (Semaphore)
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, 1); // 初始值1
sem_wait(&sem); // P操作,值减1,若为0则阻塞
// 临界区
sem_post(&sem); // V操作,值加1,唤醒等待线程
sem_destroy(&sem);
6. Windows线程资源同步对象
6.1 临界区 (CRITICAL_SECTION)
特点:轻量级,只能在同一进程内使用。
#include <Windows.h>
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
// 临界区代码
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
6.2 互斥体 (Mutex)
特点:内核对象,可跨进程使用。
HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
WaitForSingleObject(hMutex, INFINITE);
// 临界区代码
ReleaseMutex(hMutex);
CloseHandle(hMutex);
6.3 信号量 (Semaphore)
HANDLE hSem = CreateSemaphore(NULL, 1, 1, NULL);
WaitForSingleObject(hSem, INFINITE);
// 临界区代码
ReleaseSemaphore(hSem, 1, NULL);
CloseHandle(hSem);
6.4 事件 (Event)
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// 等待事件
WaitForSingleObject(hEvent, INFINITE);
// 触发事件
SetEvent(hEvent);
// 重置事件
ResetEvent(hEvent);
CloseHandle(hEvent);
6.5 Windows同步对象对比
| 对象 | 类型 | 跨进程 | 适用场景 |
|---|---|---|---|
| 临界区 | 用户态 | ❌ | 单进程内高性能同步 |
| Mutex | 内核态 | ✓ | 跨进程同步 |
| 信号量 | 内核态 | ✓ | 资源计数控制 |
| 事件 | 内核态 | ✓ | 线程通知/等待 |
7. C++11/14/17线程同步对象
7.1 std::mutex 系列
| 类型 | 版本 | 说明 |
|---|---|---|
std::mutex |
C++11 | 基本互斥量 |
std::timed_mutex |
C++11 | 支持超时的互斥量 |
std::recursive_mutex |
C++11 | 可递归加锁的互斥量 |
std::shared_mutex |
C++17 | 读写锁 |
7.2 基本使用
#include <mutex>
std::mutex mtx;
// 方式一:手动加解锁
mtx.lock();
// 临界区
mtx.unlock();
// 方式二:RAII方式(推荐)
{
std::lock_guard<std::mutex> lock(mtx);
// 临界区
} // 自动解锁
7.3 RAII 锁管理对象
| 类型 | 版本 | 说明 |
|---|---|---|
std::lock_guard |
C++11 | 作用域内自动加解锁 |
std::unique_lock |
C++11 | 更灵活,可手动加解锁 |
std::shared_lock |
C++14 | 用于 shared_mutex 的读锁 |
std::scoped_lock |
C++17 | 同时管理多个互斥量 |
7.4 读写锁 (std::shared_mutex)
适用场景:多读少写,提高并发性能。
#include <shared_mutex>
class ThreadSafeData {
public:
int read() const {
std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁(共享)
return data_;
}
void write(int value) {
std::unique_lock<std::shared_mutex> lock(mutex_); // 写锁(独占)
data_ = value;
}
private:
mutable std::shared_mutex mutex_;
int data_ = 0;
};
7.5 条件变量 (std::condition_variable)
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> dataQueue;
// 生产者
void produce(int value) {
{
std::lock_guard<std::mutex> lock(mtx);
dataQueue.push(value);
}
cv.notify_one(); // 通知一个消费者
}
// 消费者
void consume() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !dataQueue.empty(); }); // 等待条件满足
int value = dataQueue.front();
dataQueue.pop();
lock.unlock();
// 处理数据
}
💡 wait 的第二个参数:谓词函数,避免虚假唤醒。
7.6 性能对比:mutex vs shared_mutex
在8个读线程 + 1个写线程的测试中:
| 平台 | std::mutex | std::shared_mutex |
|---|---|---|
| Windows | 较慢 | 较快 |
| Linux | 取决于配置 | 读多写少时更优 |
结论:读多写少场景,优先使用 std::shared_mutex。
8. 如何确保创建的线程一定运行起来
8.1 问题
创建线程后,如何确保线程函数已经开始执行?
8.2 解决方案
方法一:使用条件变量
std::mutex mtx;
std::condition_variable cv;
bool threadStarted = false;
void thread_func() {
{
std::lock_guard<std::mutex> lock(mtx);
threadStarted = true;
}
cv.notify_one();
// 线程实际工作...
}
int main() {
std::thread t(thread_func);
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return threadStarted; });
// 现在可以确定线程已开始运行
t.join();
}
方法二:使用 promise/future
#include <future>
void thread_func(std::promise<void> started) {
started.set_value(); // 通知线程已启动
// 线程实际工作...
}
int main() {
std::promise<void> started;
std::future<void> future = started.get_future();
std::thread t(thread_func, std::move(started));
future.wait(); // 等待线程启动
// 现在可以确定线程已开始运行
t.join();
}
9. 多线程使用锁实践经验总结
9.1 减少锁的使用
原则:能不用锁就不用锁。
替代方案:
- 无锁队列
- 原子操作
- 线程局部存储
9.2 明确锁的范围
❌ 错误示例:
if (hashtable.is_empty()) {
pthread_mutex_lock(&mutex);
htable_insert(hashtable, &elem);
pthread_mutex_unlock(&mutex);
}
✓ 正确示例:
pthread_mutex_lock(&mutex);
if (hashtable.is_empty()) {
htable_insert(hashtable, &elem);
}
pthread_mutex_unlock(&mutex);
9.3 减小锁的粒度
原则:锁的临界区越小越好。
❌ 锁粒度太大:
void addTask(Task* task) {
std::lock_guard<std::mutex> lock(mutex_);
auto spTask = std::make_shared<Task>(task);
taskList_.push_back(spTask);
cv_.notify_one();
}
✓ 减小锁粒度:
void addTask(Task* task) {
auto spTask = std::make_shared<Task>(task); // 锁外创建
{
std::lock_guard<std::mutex> lock(mutex_);
taskList_.push_back(spTask); // 只保护队列操作
}
cv_.notify_one(); // 锁外通知
}
9.4 避免死锁的建议
1️⃣ 使用 RAII 自动解锁
void func() {
std::lock_guard<std::mutex> lock(mtx);
// 多个 return 路径,自动解锁
if (cond1) return;
if (cond2) return;
// ...
}
2️⃣ 线程退出时释放锁
临时线程退出前,必须释放持有的锁。
3️⃣ 多锁请求顺序一致
// ❌ 可能死锁
// 线程1: lock(A) -> lock(B)
// 线程2: lock(B) -> lock(A)
// ✓ 正确:顺序一致
// 线程1: lock(A) -> lock(B)
// 线程2: lock(A) -> lock(B)
4️⃣ 使用 scoped_lock (C++17)
std::mutex mtx1, mtx2;
void func() {
std::scoped_lock lock(mtx1, mtx2); // 避免死锁
// 临界区
}
9.5 避免活锁
活锁:多个线程使用 trylock 时相互谦让,导致谁也拿不到锁。
建议:避免过多线程使用 trylock。
10. 线程局部存储
10.1 什么是线程局部存储?
线程局部存储(Thread Local Storage, TLS):每个线程都有一份独立的数据副本,互不干扰。
10.2 Windows TLS
方式一:API
DWORD index = TlsAlloc(); // 分配索引
TlsSetValue(index, pData); // 设置值
void* pData = TlsGetValue(index); // 获取值
TlsFree(index); // 释放索引
方式二:关键字
__declspec(thread) int g_mydata = 1;
10.3 Linux TLS
方式一:API
pthread_key_t key;
pthread_key_create(&key, destructor); // 创建,可指定析构函数
pthread_setspecific(key, pData); // 设置值
void* pData = pthread_getspecific(key); // 获取值
pthread_key_delete(key); // 删除
方式二:关键字
__thread int g_mydata = 1;
10.4 C++11 thread_local(跨平台推荐)
thread_local int g_counter = 0;
void thread_func() {
g_counter++; // 每个线程有自己的副本
}
int main() {
std::thread t1(thread_func);
std::thread t2(thread_func);
t1.join();
t2.join();
// t1 和 t2 各自操作自己的 g_counter
}
10.5 注意事项
- 每个线程有独立副本,互不干扰
- 线程退出时,数据自动销毁
- 不要存储大数据,TLS 空间有限
11. C++标准库容器的线程安全性(截止C++14)
11.1 核心结论:STL容器默认不是线程安全的
⚠️ 重要:C++标准库容器(vector、string、map、set等)默认不提供线程安全保证。
为什么STL容器不保证线程安全?
| 原因 | 解释 |
|---|---|
| 性能优先 | "不为不使用的功能付出代价"是C++核心设计哲学 |
| 锁开销 | 线程安全需要加锁,会带来显著性能损失 |
| 使用场景 | 很多程序是单线程的,不需要线程安全 |
| 灵活性 | 让程序员根据需求自行添加同步机制 |
11.2 C++标准规定的线程安全规则
根据C++11/C++14标准,STL容器的线程安全遵循以下三条基本规则:
规则一:多个线程可以同时读取同一个容器
std::vector<int> vec = {1, 2, 3, 4, 5};
// 线程1:读取
void thread1() {
int size = vec.size(); // ✓ 安全
bool empty = vec.empty(); // ✓ 安全
int val = vec[0]; // ✓ 安全
}
// 线程2:同时读取
void thread2() {
for (auto it = vec.begin(); it != vec.end(); ++it) {
// ✓ 安全,只读操作
}
}
💡 原理:
const成员函数(如size()、empty()、begin())不会修改容器内部状态。
规则二:不同容器的操作完全独立
std::vector<int> vecA;
std::vector<int> vecB;
// 线程1操作vecA
void thread1() {
vecA.push_back(1); // ✓ 安全
}
// 线程2操作vecB
void thread2() {
vecB.push_back(2); // ✓ 安全
}
规则三:写操作必须独占访问
std::vector<int> vec;
// ❌ 错误:多线程同时写入
void thread1() {
vec.push_back(1); // 危险!
}
void thread2() {
vec.push_back(2); // 危险!
}
结果:数据竞争、内存损坏、程序崩溃。
11.3 各容器线程安全详细分析
📦 vector
| 操作类型 | 线程安全 | 说明 |
|---|---|---|
size(), empty() |
✓ 只读,多线程安全 | const成员函数 |
operator[] 读取 |
✓ 只读,多线程安全 | 不修改容器 |
operator[] 写入 |
✗ 不安全 | 修改元素值 |
push_back() |
✗ 不安全 | 可能触发扩容 |
pop_back() |
✗ 不安全 | 修改容器大小 |
insert() |
✗ 不安全 | 修改容器结构 |
erase() |
✗ 不安全 | 修改容器结构 |
clear() |
✗ 不安全 | 修改容器 |
vector扩容的线程安全问题:
std::vector<int> vec;
// 线程1
void thread1() {
for (int i = 0; i < 1000; ++i) {
vec.push_back(i); // 可能触发扩容
}
}
// 线程2
void thread2() {
for (int i = 0; i < 1000; ++i) {
vec.push_back(i + 1000); // 可能同时扩容
}
}
问题:
- 两个线程同时触发扩容
- 内存重新分配
- 迭代器全部失效
- 数据丢失或崩溃
📝 string
| 操作类型 | 线程安全 | 说明 |
|---|---|---|
size(), empty() |
✓ 只读,多线程安全 | const成员函数 |
c_str(), data() |
✓ 只读,多线程安全 | 获取字符串指针 |
operator[] 读取 |
✓ 只读,多线程安全 | |
operator[] 写入 |
✗ 不安全 | 修改字符 |
append() |
✗ 不安全 | 追加字符串 |
+= |
✗ 不安全 | 追加操作 |
clear() |
✗ 不安全 | 清空字符串 |
string的特殊问题 - 写时复制(C++11之前):
在C++11之前的某些实现中,string使用写时复制(Copy-on-Write)优化。
// C++03 可能的问题
std::string str = "hello";
std::string str2 = str; // 共享底层数据
// 线程1
char& c = str[0]; // 触发写时复制?
// 线程2
char& c2 = str2[0]; // 同时触发写时复制?
C++11起:string不再使用写时复制,每次拷贝都是深拷贝,避免了这个问题。
🗺️ map / set / unordered_map / unordered_set
| 操作类型 | 线程安全 | 说明 |
|---|---|---|
find() |
✓ 只读,多线程安全 | 查找操作 |
count() |
✓ 只读,多线程安全 | 计数操作 |
operator[] |
✗ 特别注意 | 可能插入新元素! |
insert() |
✗ 不安全 | 修改容器 |
erase() |
✗ 不安全 | 修改容器 |
clear() |
✗ 不安全 | 修改容器 |
map的operator[]陷阱:
std::map<int, std::string> map;
// ❌ 看似读取,实则可能写入!
void thread1() {
std::string val = map[0]; // 如果key不存在,会插入新元素!
}
void thread2() {
map[1] = "value"; // 明确写入
}
正确做法:
// ✓ 使用find进行纯查找
auto it = map.find(0);
if (it != map.end()) {
std::string val = it->second;
}
// ✓ 使用at进行只读访问(key不存在会抛异常,不会插入)
try {
std::string val = map.at(0);
} catch (const std::out_of_range&) {
// key不存在
}
📚 list / deque / queue / stack
| 容器 | 线程安全操作 | 非线程安全操作 |
|---|---|---|
list |
size(), empty(), begin()等只读操作 |
push_front(), push_back(), insert(), erase() |
deque |
只读操作 | 两端插入删除 |
queue |
无(所有操作都修改容器) | push(), pop(), front(), back() |
stack |
无(所有操作都修改容器) | push(), pop(), top() |
11.4 迭代器的线程安全问题
迭代器不是线程安全的!
std::vector<int> vec = {1, 2, 3, 4, 5};
// 线程1:遍历
void thread1() {
for (auto it = vec.begin(); it != vec.end(); ++it) {
// 读取元素
int val = *it;
// ❌ 如果此时线程2修改了vec,迭代器失效!
}
}
// 线程2:修改容器
void thread2() {
vec.push_back(6); // 可能导致重新分配内存
}
迭代器失效规则:
| 容器 | 导致迭代器失效的操作 |
|---|---|
vector |
push_back()(可能扩容)、insert()、erase() |
string |
任何可能改变长度的操作 |
deque |
两端插入/删除 |
list |
仅指向被删除元素的迭代器失效 |
map/set |
仅指向被删除元素的迭代器失效 |
11.5 如何使STL容器线程安全
方案一:使用互斥锁(最常用)
#include <vector>
#include <mutex>
#include <thread>
template<typename T>
class ThreadSafeVector {
public:
void push_back(const T& value) {
std::lock_guard<std::mutex> lock(mutex_);
vec_.push_back(value);
}
void pop_back() {
std::lock_guard<std::mutex> lock(mutex_);
vec_.pop_back();
}
T& operator[](size_t index) {
std::lock_guard<std::mutex> lock(mutex_);
return vec_[index];
}
size_t size() const {
std::lock_guard<std::mutex> lock(mutex_);
return vec_.size();
}
bool empty() const {
std::lock_guard<std::mutex> lock(mutex_);
return vec_.empty();
}
private:
std::vector<T> vec_;
mutable std::mutex mutex_; // mutable允许const函数加锁
};
方案二:使用读写锁(读多写少场景)
#include <shared_mutex>
template<typename T>
class ThreadSafeVector {
public:
void push_back(const T& value) {
std::unique_lock<std::shared_mutex> lock(mutex_); // 写锁
vec_.push_back(value);
}
T get(size_t index) const {
std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁
return vec_[index];
}
size_t size() const {
std::shared_lock<std::shared_mutex> lock(mutex_); // 读锁
return vec_.size();
}
private:
std::vector<T> vec_;
mutable std::shared_mutex mutex_;
};
方案三:使用线程安全容器库
| 库 | 特点 |
|---|---|
| Intel TBB | concurrent_vector, concurrent_queue, concurrent_hash_map |
| Boost.Lockfree | 无锁队列、无锁栈 |
| C++17并行算法 | std::for_each(std::execution::par, ...) |
11.6 常见错误与正确做法
❌ 错误1:多线程同时push_back
std::vector<int> vec;
void thread_func(int id) {
for (int i = 0; i < 1000; ++i) {
vec.push_back(id * 1000 + i); // 危险!
}
}
int main() {
std::thread t1(thread_func, 1);
std::thread t2(thread_func, 2);
t1.join();
t2.join();
// vec的大小可能不是2000!
}
✓ 正确做法
std::vector<int> vec;
std::mutex mtx;
void thread_func(int id) {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
vec.push_back(id * 1000 + i);
}
}
❌ 错误2:读取时容器被修改
std::vector<int> vec = {1, 2, 3};
void reader() {
for (auto val : vec) { // 遍历过程中
// 如果另一个线程修改vec,迭代器失效
}
}
void writer() {
vec.push_back(4); // 危险!
}
✓ 正确做法
void reader() {
std::shared_lock<std::shared_mutex> lock(mtx); // 读锁
for (auto val : vec) {
// 安全读取
}
}
void writer() {
std::unique_lock<std::shared_mutex> lock(mtx); // 写锁
vec.push_back(4);
}
❌ 错误3:返回容器内部引用
template<typename T>
class UnsafeWrapper {
std::vector<T> vec_;
public:
T& get(size_t index) {
std::lock_guard<std::mutex> lock(mtx_);
return vec_[index]; // ❌ 返回引用后锁已释放!
} // 锁在这里释放
// 调用者使用引用时,vec_可能被其他线程修改
};
✓ 正确做法
template<typename T>
class SafeWrapper {
std::vector<T> vec_;
public:
T get(size_t index) { // 返回值而不是引用
std::lock_guard<std::mutex> lock(mtx_);
return vec_[index];
}
// 或者提供带锁的操作
void modify(size_t index, const T& value) {
std::lock_guard<std::mutex> lock(mtx_);
vec_[index] = value;
}
};
11.7 总结
| 要点 | 说明 |
|---|---|
| 默认不安全 | STL容器不提供线程安全保证 |
| 多读安全 | 多个线程可以同时读取同一容器 |
| 写需独占 | 写操作必须独占访问 |
| 迭代器失效 | 容器修改可能导致迭代器失效 |
| 自己加锁 | 需要线程安全时,自行添加同步机制 |
| 考虑专用库 | 高并发场景使用TBB等并发容器库 |
12. C库的非线程安全函数
12.1 常见非线程安全函数
| 函数 | 问题 | 替代方案 |
|---|---|---|
strtok |
使用静态缓冲区 | strtok_r |
asctime |
返回静态缓冲区指针 | asctime_r |
ctime |
返回静态缓冲区指针 | ctime_r |
gmtime |
返回静态缓冲区指针 | gmtime_r |
localtime |
返回静态缓冲区指针 | localtime_r |
rand |
使用共享种子 | 使用线程安全版本或加锁 |
strerror |
返回静态缓冲区指针 | strerror_r |
12.2 示例:strtok 的问题
// ❌ 非线程安全
char* p = strtok(str, ",");
while (p != NULL) {
// 如果另一个线程也在使用 strtok,会出问题
p = strtok(NULL, ",");
}
// ✓ 线程安全版本
char* saveptr;
char* p = strtok_r(str, ",", &saveptr);
while (p != NULL) {
p = strtok_r(NULL, ",", &saveptr);
}
13. 线程池与队列系统的设计
13.1 线程池原理
核心思想:
- 创建一组工作线程,生命周期与程序相同
- 任务放入队列
- 工作线程从队列取任务执行
- 无任务时线程阻塞/睡眠
13.2 简单线程池实现
#include <thread>
#include <mutex>
#include <condition_variable>
#include <list>
#include <vector>
#include <memory>
class Task {
public:
virtual void doIt() {}
virtual ~Task() {}
};
class TaskPool {
public:
TaskPool() : running_(false) {}
~TaskPool() {
stop();
}
void init(int threadNum = 5) {
running_ = true;
for (int i = 0; i < threadNum; ++i) {
threads_.emplace_back([this] { workerThread(); });
}
}
void stop() {
{
std::lock_guard<std::mutex> lock(mutex_);
running_ = false;
}
cv_.notify_all();
for (auto& t : threads_) {
if (t.joinable()) t.join();
}
threads_.clear();
}
void addTask(std::shared_ptr<Task> task) {
{
std::lock_guard<std::mutex> lock(mutex_);
tasks_.push_back(task);
}
cv_.notify_one();
}
private:
void workerThread() {
while (true) {
std::shared_ptr<Task> task;
{
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] {
return !running_ || !tasks_.empty();
});
if (!running_ && tasks_.empty()) return;
task = tasks_.front();
tasks_.pop_front();
}
if (task) {
task->doIt();
}
}
}
private:
std::list<std::shared_ptr<Task>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
bool running_;
std::vector<std::thread> threads_;
};
13.3 生产者-消费者模型
本质:
- 生产者:产生任务,放入队列
- 消费者:从队列取任务处理
- 队列:解耦生产者和消费者
13.4 消息中间件
常见消息中间件:Kafka、RabbitMQ、RocketMQ、ActiveMQ
优势:
- 生产者和消费者最大化解耦
- 支持数据备份、负载均衡、容灾容错
- 适用于分布式系统
14. 纤程(Fiber)与协程(Coroutine)
14.1 纤程 (Fiber) - Windows 特有
什么是纤程?
- 用户级线程,由程序控制切换
- 一个线程可包含多个纤程
- 切换开销小,无需内核参与
纤程 vs 线程:
| 特性 | 线程 | 纤程 |
|---|---|---|
| 调度 | 操作系统内核 | 用户程序 |
| 切换开销 | 大 | 小 |
| 抢占式 | 是 | 否(协作式) |
Windows 纤程 API:
// 转换为纤程模式
LPVOID mainFiber = ConvertThreadToFiber(NULL);
// 创建纤程
LPVOID workerFiber = CreateFiber(0, FiberProc, NULL);
// 切换到纤程
SwitchToFiber(workerFiber);
// 删除纤程
DeleteFiber(workerFiber);
// 切回线程模式
ConvertFiberToThread();
14.2 协程 (Coroutine)
什么是协程?
- 应用层模拟的线程
- 避免线程上下文切换开销
- 协作式调度(非抢占)
协程的优势:
- 高并发网络服务器:一个协程处理一个连接
- 代码清晰:避免异步回调的复杂性
- 性能好:减少上下文切换
协程的实现原理:
- 维护一组数据结构和 n 个线程
- 协程代码放入待执行队列
- 线程从队列取出协程执行
- 遇到 IO 操作时,保存状态,切换到其他协程
主流协程实现:
- Golang goroutine:最成熟,语言层面支持
- 腾讯 libco:C/C++ 协程库
- C++20 协程:语言层面支持
14.3 协程 vs 线程 vs 纤程
| 特性 | 线程 | 纤程 | 协程 |
|---|---|---|---|
| 调度方式 | 抢占式 | 协作式 | 协作式 |
| 切换开销 | 大 | 小 | 小 |
| 多核利用 | ✓ | ✗ | 需配合多线程 |
| 跨平台 | ✓ | Windows | ✓ |
15. C++异步编程:async / future / promise
15.1 为什么需要异步编程?
创建线程时,获取线程的返回值很不方便。std::thread 不支持直接获取返回值,而 std::async + std::future 提供了优雅的解决方案。
15.2 std::promise 与 std::future
核心思想:promise 在子线程中"承诺"设置一个值,future 在主线程中"等待"获取这个值。
#include <future>
#include <iostream>
#include <thread>
void calculate(std::promise<int> prom) {
// 执行耗时计算
int result = 42;
prom.set_value(result); // 设置结果(子线程)
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future(); // 获取 future
std::thread t(calculate, std::move(prom));
int result = fut.get(); // 等待并获取结果(主线程阻塞)
std::cout << "Result: " << result << std::endl;
t.join();
return 0;
}
关键API:
| 类/方法 | 说明 |
|---|---|
std::promise<T> |
在子线程中设置值 |
promise.set_value(val) |
设置值 |
promise.set_exception(e) |
设置异常 |
std::future<T> |
在主线程中获取值 |
future.get() |
阻塞等待并获取值 |
future.wait() |
只等待,不获取值 |
future.wait_for(dur) |
超时等待 |
future.wait_until(time) |
截止时间等待 |
15.3 std::async — 更简洁的异步调用
std::async 是对 std::thread + std::promise + std::future 的高级封装。
#include <future>
#include <iostream>
int calculate(int a, int b) {
// 模拟耗时计算
std::this_thread::sleep_for(std::chrono::seconds(2));
return a + b;
}
int main() {
// 自动创建线程并返回 future
std::future<int> fut = std::async(std::launch::async, calculate, 10, 20);
// 做其他事情...
std::cout << "Doing other work..." << std::endl;
// 需要结果时再等待
int result = fut.get();
std::cout << "Result: " << result << std::endl; // 输出 30
return 0;
}
启动策略:
| 策略 | 说明 |
|---|---|
std::launch::async |
强制创建新线程 |
std::launch::deferred |
延迟执行,调用 get() 时才在当前线程执行 |
std::launch::async \| std::launch::deferred |
默认值,由编译器决定 |
⚠️ 注意:不指定策略时,编译器可能选择延迟执行。如果需要真正的异步,建议显式指定
std::launch::async。
15.4 std::future 的其他用法
获取多个异步结果
#include <future>
#include <vector>
#include <iostream>
int main() {
std::vector<std::future<int>> futures;
for (int i = 0; i < 5; ++i) {
futures.push_back(std::async(std::launch::async, [i] {
return i * i;
}));
}
for (auto& fut : futures) {
std::cout << fut.get() << " "; // 0 1 4 9 16
}
return 0;
}
std::shared_future — 多个线程等待同一结果
std::promise<int> prom;
std::shared_future<int> sfut = prom.get_future().share();
// 多个线程都可以调用 sfut.get()
std::thread t1([sfut]() { std::cout << sfut.get() << std::endl; });
std::thread t2([sfut]() { std::cout << sfut.get() << std::endl; });
超时等待
std::future<int> fut = std::async(std::launch::async, calculate, 10, 20);
if (fut.wait_for(std::chrono::seconds(3)) == std::future_status::ready) {
std::cout << "Result: " << fut.get() << std::endl;
} else {
std::cout << "Timeout!" << std::endl;
}
15.5 线程中的异常处理
子线程抛出的异常不会直接传递到主线程,需要通过 std::exception_ptr 传递。
#include <future>
#include <iostream>
void may_throw(std::promise<int> prom) {
try {
throw std::runtime_error("Something went wrong!");
} catch (...) {
prom.set_exception(std::current_exception()); // 捕获并传递异常
}
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(may_throw, std::move(prom));
try {
int result = fut.get(); // 这里会重新抛出异常
} catch (const std::exception& e) {
std::cout << "Caught: " << e.what() << std::endl;
}
t.join();
return 0;
}
💡 使用
std::async时更简单:异常会自动通过future.get()重新抛出。
16. std::call_once 与线程安全单例
16.1 std::call_once
问题:如何确保某个函数在多线程环境下只被执行一次?
解决方案:std::call_once + std::once_flag。
#include <mutex>
#include <iostream>
std::once_flag flag;
void initialize() {
std::cout << "Initializing..." << std::endl;
// 只会执行一次的初始化代码
}
void thread_func() {
std::call_once(flag, initialize); // 保证 initialize 只执行一次
}
特点:
- 即使多个线程同时调用
std::call_once,函数也只执行一次 - 内部实现高效:第一次调用后,后续调用不再加锁
- 比"手动加锁+标志位"更安全、更高效
16.2 线程安全的单例模式
方式一:Meyers' Singleton(推荐,C++11起保证线程安全)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
💡 为什么C++11的局部静态变量是线程安全的?
C++11标准规定:如果多个线程同时初始化同一个局部静态变量,只有一个线程会完成初始化,其他线程会等待初始化完成。编译器通常通过内部加锁来实现。
方式二:使用 std::call_once
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag, []() {
instance = new Singleton();
});
return *instance;
}
private:
Singleton() = default;
static Singleton* instance;
static std::once_flag flag;
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::flag;
17. 内存序(memory_order)详解
17.1 为什么需要内存序?
现代CPU和编译器会对指令进行重排序以提高性能,这可能导致多线程程序出现意料之外的行为。
一个经典例子:
// 线程1
int data = 0;
bool ready = false;
void thread1() {
data = 42; // ①
ready = true; // ②
}
// 线程2
void thread2() {
if (ready) { // ③
// data一定是42吗?不一定!
// 编译器或CPU可能把①和②重排序
// 导致线程2看到ready=true但data还是0
assert(data == 42); // 可能失败!
}
}
17.2 六种内存序
| 内存序 | 约束强度 | 说明 |
|---|---|---|
memory_order_relaxed |
最弱 | 只保证原子性,不保证顺序 |
memory_order_consume |
弱 | 很少使用,依赖关系排序 |
memory_order_acquire |
中 | 获取语义:阻止后面的读写被重排到前面 |
memory_order_release |
中 | 释放语义:阻止前面的读写被重排到后面 |
memory_order_acq_rel |
强 | 获取+释放,用于读改写操作 |
memory_order_seq_cst |
最强 | 顺序一致性(默认值),全局总排序 |
17.3 图解 acquire-release 语义
线程A(生产者) 线程B(消费者)
┌─────────────────┐ ┌─────────────────┐
│ 写入 data = 42 │ │ │
│ ↓ │ │ │
│ store(ready, │──release──→──│ load(ready) │
│ release) │ 同步 │ acquire) │
│ │ │ ↓ │
└─────────────────┘ │ 读取 data │
│ (保证看到42) │
└─────────────────┘
规则:
release:之前的所有写操作对其他线程可见acquire:之后的读操作能看到release之前的所有写操作- 配对使用:一个线程
release,另一个线程acquire,形成同步关系
17.4 各内存序使用场景
relaxed — 计数器(不关心顺序)
std::atomic<int> counter{0};
// 多线程递增计数器
counter.fetch_add(1, std::memory_order_relaxed);
💡 只关心原子性,不关心操作顺序。适用于统计计数等场景。
acquire-release — 标志位同步
std::atomic<bool> ready{false};
int data = 0;
// 生产者线程
data = 42;
ready.store(true, std::memory_order_release); // release
// 消费者线程
while (!ready.load(std::memory_order_acquire)) {} // acquire
// 此时保证 data == 42
seq_cst — 默认值(最安全但最慢)
std::atomic<int> x{0};
std::atomic<int> y{0};
x.store(1); // 默认 memory_order_seq_cst
y.store(2); // 默认 memory_order_seq_cst
💡 如果不确定用哪个,就用默认的
seq_cst。性能差异通常不大,但正确性最重要。
17.5 何时使用哪种内存序?
| 场景 | 推荐内存序 |
|---|---|
| 不确定 | seq_cst(默认) |
| 简单计数器 | relaxed |
| 标志位同步(写-读配对) | release + acquire |
| 双重检查锁定 | acquire + release |
| 高性能无锁数据结构 | relaxed + acq_rel |
⚠️ 建议:除非有明确的性能需求,否则使用默认的
seq_cst即可。
18. 双重检查锁定(DCLP)
18.1 什么是双重检查锁定?
双重检查锁定(Double-Checked Locking Pattern, DCLP) 是一种优化单例初始化的模式:第一次检查不加锁(快速路径),只有需要初始化时才加锁。
18.2 错误的DCLP(C++11之前)
// ❌ 错误!在C++11之前不安全
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查(无锁)
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) { // 第二次检查(有锁)
instance = new Singleton(); // 危险!
}
}
return instance;
}
private:
static Singleton* instance;
static std::mutex mtx;
};
为什么错误?
new Singleton() 实际上分三步:
- 分配内存
- 调用构造函数
- 将指针赋值给
instance
编译器和CPU可能将②和③重排序为 1→3→2,导致其他线程看到 instance != nullptr 但对象尚未构造完成。
18.3 C++11 修复后的DCLP
// ✓ 正确:使用 std::atomic + memory_order
class Singleton {
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
Singleton() = default;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
18.4 最佳实践:Meyers' Singleton
// ✓ 最推荐的方式(C++11起)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 编译器保证线程安全
return instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
💡 结论:除非有特殊需求,否则直接使用 Meyers' Singleton,简单且安全。
19. 数据竞争 vs 竞争条件
19.1 两个容易混淆的概念
数据竞争(Data Race)
定义:两个或多个线程同时访问同一内存位置,且至少一个是写操作,且没有同步。
结果:未定义行为(Undefined Behavior),程序可能崩溃、数据错乱。
int counter = 0;
// ❌ 数据竞争
void thread1() { counter++; } // 写
void thread2() { counter++; } // 写,无同步
竞争条件(Race Condition)
定义:程序的输出依赖于线程的执行顺序。
结果:逻辑错误,但不是未定义行为。
std::atomic<int> ticket{0};
// ⚠️ 竞争条件(但不是数据竞争,因为有原子操作)
int getTicket() {
return ticket.fetch_add(1); // 原子操作,无数据竞争
}
// 问题是:哪个线程先拿到票取决于调度顺序
// 这可能导致逻辑上不正确(如VIP应该先拿票)
19.2 对比总结
| 特性 | 数据竞争 | 竞争条件 |
|---|---|---|
| 本质 | 缺少同步的并发内存访问 | 程序结果依赖执行时序 |
| 结果 | 未定义行为 | 逻辑错误 |
| 严重性 | 程序可能崩溃 | 程序逻辑不正确 |
| 解决 | 加锁/原子操作 | 重新设计逻辑/加同步 |
| 检测 | ThreadSanitizer等工具 | 代码审查/测试 |
19.3 重要区别
💡 数据竞争一定是竞争条件,但竞争条件不一定是数据竞争。
- 数据竞争是语言层面的问题(C++标准定义)
- 竞争条件是逻辑层面的问题(设计缺陷)
// 示例:有数据竞争
int x = 0;
// 线程1: x = 1
// 线程2: x = 2
// 结果不确定 → 数据竞争 + 竞争条件
// 示例:有竞争条件但无数据竞争
std::mutex mtx;
int x = 0;
// 线程1: lock; if(x==0) x=1; unlock;
// 线程2: lock; if(x==0) x=1; unlock;
// 最终 x=1(正确),但哪个线程"赢了"不确定 → 竞争条件
20. std::shared_ptr 的线程安全性
20.1 核心结论
| 方面 | 线程安全 | 说明 |
|---|---|---|
| 控制块(引用计数) | ✓ 安全 | 多线程可安全拷贝/销毁 shared_ptr |
| 指向的对象 | ✗ 不安全 | 多线程读写对象需要额外同步 |
| shared_ptr 本身 | ✓ 安全 | 多线程可同时操作不同的 shared_ptr 对象 |
20.2 详细分析
安全的操作
std::shared_ptr<int> globalPtr = std::make_shared<int>(42);
// ✓ 安全:多个线程拷贝 shared_ptr(只影响引用计数)
void thread1() {
auto local = globalPtr; // 引用计数原子递增
}
void thread2() {
auto local = globalPtr; // 引用计数原子递增
}
不安全的操作
std::shared_ptr<int> globalPtr = std::make_shared<int>(42);
// ❌ 不安全:多线程修改指向的对象
void thread1() {
*globalPtr = 100; // 修改对象
}
void thread2() {
int val = *globalPtr; // 读取对象
}
// ❌ 不安全:多线程修改 shared_ptr 本身(改变指向)
void thread1() {
globalPtr = std::make_shared<int>(200); // 改变指向
}
正确做法
std::shared_ptr<int> globalPtr = std::make_shared<int>(42);
std::mutex mtx;
void thread1() {
std::lock_guard<std::mutex> lock(mtx);
*globalPtr = 100;
}
void thread2() {
std::lock_guard<std::mutex> lock(mtx);
int val = *globalPtr;
}
20.3 为什么引用计数是线程安全的?
std::shared_ptr 的控制块中的引用计数使用原子操作实现,所以多个线程同时拷贝或销毁 shared_ptr 是安全的。
shared_ptr 对象 控制块
┌──────────────┐ ┌──────────────┐
│ ptr_ ───────────────→ │ 对象数据 │
│ ref_count ──────────→ │ 强引用计数 │ ← atomic<int>
│ weak_count ─────────→ │ 弱引用计数 │ ← atomic<int>
└──────────────┘ └──────────────┘
21. 无锁编程简介
21.1 什么是无锁编程?
无锁编程(Lock-Free Programming) 是一种不使用互斥锁来实现多线程同步的技术,主要依赖原子操作(CAS)。
21.2 CAS(Compare-And-Swap)
CAS 是无锁编程的核心原语:比较当前值与期望值,如果相等则替换为新值。
// std::atomic 的 CAS 操作
bool success = atomic_var.compare_exchange_weak(expected, desired);
// 如果 atomic_var == expected,则 atomic_var = desired,返回 true
// 如果 atomic_var != expected,则 expected 被更新为当前值,返回 false
21.3 无锁栈的简单实现
#include <atomic>
template<typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
Node(T val) : data(std::move(val)), next(nullptr) {}
};
public:
void push(T value) {
Node* newNode = new Node(std::move(value));
newNode->next = head.load();
// 如果 head 没被其他线程修改,则更新 head
while (!head.compare_exchange_weak(newNode->next, newNode)) {
// newNode->next 已被自动更新为当前 head 值
}
}
bool pop(T& result) {
Node* oldHead = head.load();
while (oldHead &&
!head.compare_exchange_weak(oldHead, oldHead->next)) {
// oldHead 已被自动更新为当前 head 值
}
if (oldHead) {
result = std::move(oldHead->data);
delete oldHead;
return true;
}
return false;
}
private:
std::atomic<Node*> head{nullptr};
};
21.4 ABA 问题
ABA问题是无锁编程中的经典陷阱:
线程1读取 head = A
↓ 被挂起
线程2: head = A → B → A(A被弹出,B被弹出,A又被推入)
↓ 线程1恢复
线程1: CAS(head, A, C) // 成功!但中间的B已经丢失了
解决方案:
- 使用带版本号的指针(如
std::atomic<std::shared_ptr>) - 使用 hazard pointer
- 使用 epoch-based reclamation
21.5 compare_exchange_weak vs compare_exchange_strong
| 方法 | 说明 |
|---|---|
compare_exchange_weak |
可能"虚假失败"(值相等但返回false),但通常更快 |
compare_exchange_strong |
只有值真的不相等才失败,通常用于循环外 |
// weak 版本(循环中使用,性能更好)
while (!atomic_var.compare_exchange_weak(expected, desired)) {
// 重新尝试
}
// strong 版本(只尝试一次)
bool success = atomic_var.compare_exchange_strong(expected, desired);
if (!success) {
// 处理失败
}
21.6 无锁编程的建议
| 建议 | 说明 |
|---|---|
| 能用锁就用锁 | 无锁编程复杂度高,容易出错 |
| 性能瓶颈才考虑 | 只有锁竞争确实成为瓶颈时才考虑无锁 |
| 使用成熟库 | 优先使用 Intel TBB、Boost.Lockfree 等经过验证的库 |
| 充分测试 | 无锁算法的 bug 极难复现,需要压力测试 |
22. 常见多线程设计模式
22.1 模式总览
| 模式 | 适用场景 | 核心思想 |
|---|---|---|
| 生产者-消费者 | 任务队列、消息系统 | 队列解耦生产和消费 |
| 读写锁模式 | 读多写少 | 读共享,写独占 |
| Future/Promise | 获取异步结果 | 异步执行,同步获取 |
| 对象池模式 | 资源复用 | 预创建对象,用完归还 |
| Leader-Follower | 线程池变体 | 一个线程接收任务,分发给其他线程 |
| Double-Checked Locking | 延迟初始化 | 先检查再加锁 |
22.2 RAII 在多线程中的深度应用
自定义锁包装器
template<typename Lock>
class ScopedLock {
public:
explicit ScopedLock(Lock& lock) : lock_(lock) {
lock_.lock();
}
~ScopedLock() {
lock_.unlock();
}
// 禁止拷贝
ScopedLock(const ScopedLock&) = delete;
ScopedLock& operator=(const ScopedLock&) = delete;
private:
Lock& lock_;
};
// 使用
pthread_mutex_t mtx;
{
ScopedLock<pthread_mutex_t> guard(mtx);
// 临界区
} // 自动解锁
scope_guard(C++11 实现)
template<typename F>
class ScopeGuard {
public:
explicit ScopeGuard(F f) : func_(std::move(f)) {}
~ScopeGuard() { func_(); }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
private:
F func_;
};
// 使用
ScopeGuard<std::function<void()>> guard([&]() {
pthread_mutex_unlock(&mtx);
});
💡 C++11 的
std::lock_guard和std::unique_lock已经是 RAII 锁管理的标准实现,日常开发直接使用它们即可。
23. 本章总结
23.1 知识图谱
多线程编程
├── 基础概念
│ ├── 进程与线程
│ ├── 线程创建与退出
│ └── 线程函数传参
├── 同步机制
│ ├── 原子操作(std::atomic)
│ ├── 内存序(memory_order)
│ ├── 互斥锁(mutex / lock_guard / unique_lock)
│ ├── 读写锁(shared_mutex)
│ ├── 条件变量(condition_variable)
│ └── 信号量(semaphore)
├── 异步编程
│ ├── std::async
│ ├── std::future / std::promise
│ ├── std::shared_future
│ └── 线程异常处理
├── 线程安全
│ ├── STL容器线程安全
│ ├── std::shared_ptr线程安全
│ ├── std::call_once
│ └── 数据竞争 vs 竞争条件
├── 设计模式
│ ├── 双重检查锁定(DCLP)
│ ├── 线程安全单例
│ ├── 线程池
│ ├── 生产者-消费者
│ └── RAII锁管理
├── 高级主题
│ ├── 线程局部存储(TLS)
│ ├── 无锁编程(CAS / ABA问题)
│ ├── 纤程(Fiber)
│ └── 协程(Coroutine)
└── 最佳实践
├── 减少锁使用
├── 减小锁粒度
└── 避免死锁/活锁
23.2 核心要点
- 多线程编程是后端开发的基本功
- 理解操作系统原理是关键
- 跨平台优先使用 C++11/14 标准库
- 锁的使用要谨慎:减少使用、明确范围、减小粒度
- 线程池和消息队列是常用架构模式
- 协程是高并发场景的利器
- std::async/future/promise 是现代异步编程的标准方式
- 内存序是多线程编程的进阶知识,默认用 seq_cst 即可
- 区分数据竞争(未定义行为)和竞争条件(逻辑错误)
- 无锁编程复杂度高,优先使用锁,性能瓶颈时再考虑
23.3 学习建议
- 从操作系统原理出发理解多线程
- 多动手实践,熟悉 API 层面的使用
- 学习优秀的开源项目(如 Redis、muduo)
- 关注 C++ 新标准的演进
- 使用 ThreadSanitizer 等工具检测数据竞争
- 从简单的锁开始,逐步深入到无锁编程
附录:常用 API 速查表
Linux pthread
| 函数 | 作用 |
|---|---|
pthread_create |
创建线程 |
pthread_join |
等待线程结束 |
pthread_mutex_init/destroy |
初始化/销毁互斥量 |
pthread_mutex_lock/unlock |
加锁/解锁 |
pthread_cond_init/destroy |
初始化/销毁条件变量 |
pthread_cond_wait |
等待条件变量 |
pthread_cond_signal/broadcast |
唤醒等待线程 |
C++11/14
| 类/函数 | 作用 |
|---|---|
std::thread |
线程类 |
std::mutex |
互斥量 |
std::shared_mutex |
读写锁 (C++17) |
std::condition_variable |
条件变量 |
std::lock_guard |
RAII 锁管理 |
std::unique_lock |
灵活的锁管理 |
std::atomic |
原子变量 |
thread_local |
线程局部存储关键字 |
std::async |
异步任务 |
std::future |
获取异步结果 |
std::promise |
设置异步结果 |
std::shared_future |
多个线程共享的 future |
std::call_once |
确保函数只执行一次 |
std::once_flag |
call_once 的标志 |

浙公网安备 33010602011771号