C++多线程编程

C++ 多线程编程从入门到进阶 - 完整学习笔记


目录

  1. 线程的基本概念
  2. 线程基本操作
  3. 线程函数传C++类实例指针惯用法
  4. 整型变量的原子操作
  5. Linux线程同步对象
  6. Windows线程资源同步对象
  7. C++11/14/17线程同步对象
  8. 如何确保创建的线程一定运行起来
  9. 多线程使用锁实践经验总结
  10. 线程局部存储
  11. C++标准库容器的线程安全性
  12. C库的非线程安全函数
  13. 线程池与队列系统的设计
  14. 纤程与协程
  15. C++异步编程:async/future/promise
  16. std::call_once与线程安全单例
  17. 内存序(memory_order)详解
  18. 双重检查锁定(DCLP)
  19. 数据竞争 vs 竞争条件
  20. std::shared_ptr的线程安全性
  21. 无锁编程简介
  22. 常见多线程设计模式
  23. 本章总结

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 注意事项

  1. 每个线程有独立副本,互不干扰
  2. 线程退出时,数据自动销毁
  3. 不要存储大数据,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);  // 可能同时扩容
    }
}

问题

  1. 两个线程同时触发扩容
  2. 内存重新分配
  3. 迭代器全部失效
  4. 数据丢失或崩溃

📝 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 线程池原理

核心思想

  1. 创建一组工作线程,生命周期与程序相同
  2. 任务放入队列
  3. 工作线程从队列取任务执行
  4. 无任务时线程阻塞/睡眠

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)

什么是协程?

  • 应用层模拟的线程
  • 避免线程上下文切换开销
  • 协作式调度(非抢占)

协程的优势

  • 高并发网络服务器:一个协程处理一个连接
  • 代码清晰:避免异步回调的复杂性
  • 性能好:减少上下文切换

协程的实现原理

  1. 维护一组数据结构和 n 个线程
  2. 协程代码放入待执行队列
  3. 线程从队列取出协程执行
  4. 遇到 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() 实际上分三步:

  1. 分配内存
  2. 调用构造函数
  3. 将指针赋值给 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已经丢失了

解决方案

  1. 使用带版本号的指针(如 std::atomic<std::shared_ptr>
  2. 使用 hazard pointer
  3. 使用 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_guardstd::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 核心要点

  1. 多线程编程是后端开发的基本功
  2. 理解操作系统原理是关键
  3. 跨平台优先使用 C++11/14 标准库
  4. 锁的使用要谨慎:减少使用、明确范围、减小粒度
  5. 线程池和消息队列是常用架构模式
  6. 协程是高并发场景的利器
  7. std::async/future/promise 是现代异步编程的标准方式
  8. 内存序是多线程编程的进阶知识,默认用 seq_cst 即可
  9. 区分数据竞争(未定义行为)和竞争条件(逻辑错误)
  10. 无锁编程复杂度高,优先使用锁,性能瓶颈时再考虑

23.3 学习建议

  1. 从操作系统原理出发理解多线程
  2. 多动手实践,熟悉 API 层面的使用
  3. 学习优秀的开源项目(如 Redis、muduo)
  4. 关注 C++ 新标准的演进
  5. 使用 ThreadSanitizer 等工具检测数据竞争
  6. 从简单的锁开始,逐步深入到无锁编程

附录:常用 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 的标志
posted @ 2026-05-24 10:13  灵垚克府  阅读(37)  评论(0)    收藏  举报