前言

C++98/03 时代,多线程只能靠平台 API pthread / Win32 Thread,存在三大痛点:

  1. 不可移植:pthread 和 Win32 Thread 代码完全不同,写一次只能在一个平台跑;

  2. 容易死锁:手动管理锁的 lock/unlock,极易忘记或异常时漏解锁;

  3. 异常安全差:线程崩溃、异常抛出时,资源(线程句柄、锁)难以正确回收。

C++11 委员会决定把并发直接纳入标准库,目标是 “写一次,到处线程安全运行”,同时保证异常安全和 RAII 风格。

  • C++11:引入 std::thread(原生线程)、std::mutex(锁)、std::async(高级任务)、std::atomic(无锁编程)、std::condition_variable(条件变量);

  • C++14:完善 std::shared_lock(读写锁的读锁);

  • C++17:加入 std::scoped_lock(多锁防死锁)和并行算法(std::execution::par)。

C++ 标准库的所有锁(lock_guardunique_lock 等)都不是 “锁本身”,而是RAII wrapper,锁的 “管理器”,真正的锁是 std::mutexstd::shared_mutex 这些互斥量,锁管理器只是负责帮你自动调用 lock()unlock()

温情提示:本文超多字!3.5w字警告!有需要可以点击目录跳转对应的知识点。


std::thread

设计原因

提供原生、可移植的线程创建方式,屏蔽平台 API 差异。

底层原理

内部调用操作系统线程 API,Linux 下是 pthread,Windows 下是 CreateThread;线程启动后立即执行传入的函数或 lambda;析构时若未 join/detach 会直接调用 std::terminate 崩溃(故意设计,防止资源泄漏)。

实际代码示例:

#include <iostream>
#include <thread>  // 必须包含

// C++98 老写法(Linux pthread,不可移植)
#include <pthread.h>
void* thread_func(void*) {
    std::cout << "我在新线程里运行\n";
    return nullptr;
}
pthread_t t;
pthread_create(&t, nullptr, thread_func, nullptr);
pthread_join(t, nullptr);

// C++11 新写法(可移植)
int main() {
    std::thread t([]{
        std::cout << "我在新线程里运行\n";
    });
    t.join();   // 必须 join(等待线程结束)或 detach(分离,让它自己跑)
}

线程对象析构前必须 join()等待结束 或 detach()分离。

需要注意的是,std::thread 默认拷贝参数,如果想传引用,必须用 std::ref/std::cref

#include <iostream>
#include <thread>
#include <functional>  // std::ref

void add_one(int& n) { n++; }

int main() {
    int x = 10;
    // 错误:直接传 x,thread 会拷贝一份,add_one 改的是副本
    // std::thread t1(add_one, x); t1.join(); std::cout << x << "\n"; // 输出 10

    // 正确:用 std::ref 包装 x,传递引用
    std::thread t2(add_one, std::ref(x));
    t2.join();
    std::cout << x << "\n"; // 输出 11
}

常见坑:

  • std::thread 没 join/detach 就析构 → 直接调用 std::terminate 崩溃;

  • 多把 mutex 按不同顺序 lock → 死锁(用 std::scoped_lockstd::lock 解决);

  • std::thread 传引用不用 std::ref → 传的是副本,改不动原对象;

QA:

std::thread 析构时没 join 会怎样?

调用 std::terminate(),程序直接崩溃。这是故意设计的,如果线程没 join 就析构,要么是线程还在跑(资源泄漏),要么是忘记 join(逻辑错误),直接崩溃能让开发者快速发现问题。(C++20 的 std::jthread 解决了这个问题)。


thread_local线程局部存储

设计原因

很多场景下,每个线程都需要自己独立的一份全局数据例如线程 ID、线程局部缓存、TLS 变量、随机数引擎等,而普通的全局变量或 static 变量是进程级共享的,会造成数据竞争或错误。C++11 引入 thread_local 关键字,提供了标准、可移植的线程局部存储机制。

底层原理

  • thread_local 变量在每个线程都有独立的一份实例,在线程第一次访问时才构造,在线程结束时析构。

  • 底层实现依赖操作系统线程本地存储(TLS)机制(Windows TLS slot、Linux pthread_key 等),编译器自动生成相应的线程局部代码。

  • 生命周期和线程绑定,不受其他线程影响。

实际例子:

场景 1:线程 ID

thread_local int thread_id = 0;   // 每个线程都有自己独立的 thread_id

void worker() {
    if (thread_id == 0) {
        thread_id = get_next_thread_id();   // 只在当前线程设置
    }
    std::cout << "线程 " << thread_id << " 正在工作\n";
}

场景 2:线程局部随机数引擎

thread_local std::mt19937 rng{std::random_device{}()};   // 每个线程独立的随机引擎

void monte_carlo_simulation() {
    std::uniform_real_distribution<double> dist(0.0, 1.0);
    double r = dist(rng);   // 每个线程使用自己的 rng,避免锁竞争
}

场景 3:线程局部缓存

thread_local std::unordered_map<std::string, int> cache;   // 每个线程独立的缓存

int get_value(const std::string& key) {
    if (auto it = cache.find(key); it != cache.end()) {
        return it->second;
    }
    int value = compute_expensive(key);
    cache[key] = value;
    return value;
}

性能优势

访问 thread_local 变量比加锁的全局变量快 10~100 倍(无锁竞争),特别适合高并发场景。

常见坑

  • thread_local 变量的构造 / 析构是每个线程独立的,构造时机是第一次访问时(延迟构造)。

  • 不要在 thread_local 中存指向栈上对象的指针(线程结束时栈被回收)。

  • thread_local 变量的初始化如果有副作用,要小心多线程初始化顺序。

  • 不能用于 static 成员变量,只能用于全局 / 命名空间作用域或函数局部静态

QA

thread_local 和普通 static 变量有什么本质区别?

static 是进程级共享,所有线程共用一份;thread_local 是线程级独立,每个线程都有自己的一份。

thread_local 在什么场景必须使用?

需要每个线程独立的数据如线程 ID、随机数引擎、TLS 缓存、线程局部日志上下文等,避免加锁竞争。

thread_local 的构造时机是什么?

线程第一次访问该变量时才构造,线程结束时析构。


std::lock 死锁避免机制算法

设计原因

多线程编程中,同时锁定多个互斥锁是常见需求如转账操作需同时锁定 “转出账户” 和 “转入账户” 的锁、资源池管理需同时锁定 “资源块” 和 “引用计数” 的锁。但手动按任意顺序加锁会导致循环等待,这是死锁的四大必要条件之一。

C++11 引入 std::lock 函数模板,提供了一种标准、死锁避免的原子性锁定多个互斥锁的机制,从根源上消除循环等待的风险

底层原理

std::lock 的核心是死锁避免算法,C++ 标准虽未强制规定具体实现,但要求必须满足 “死锁安全”。常见实现策略包括:

  1. 固定顺序锁定:按 mutex 对象的地址或其他唯一标识排序,所有线程都按相同顺序锁定,破坏循环等待条件。

  2. 尝试 - 回退机制:先尝试锁定所有 mutex,若失败则立即释放已锁定的 mutex,等待随机时间后重试,避免持续占用资源。

无论采用哪种策略,std::lock 都能保证:要么所有 mutex 都被成功锁定,要么一个都不锁(原子性),且不会发生死锁。

实际例子:

场景 1:经典转账操作(双锁场景)

#include <mutex>
#include <iostream>

struct Account {
    int balance;
    std::mutex mtx;
};

void transfer(Account& from, Account& to, int amount) {
    // 原子性锁定两个账户的 mutex,避免死锁
    std::lock(from.mtx, to.mtx);
    
    // 用 std::adopt_lock 告诉 lock_guard:mutex 已经锁好了,直接接管
    std::lock_guard<std::mutex> lock_from(from.mtx, std::adopt_lock);
    std::lock_guard<std::mutex> lock_to(to.mtx, std::adopt_lock);
    
    from.balance -= amount;
    to.balance += amount;
    std::cout << "转账成功: " << amount << "\n";
}

场景 2:线程池任务队列的双端操作

#include <queue>
#include <mutex>
#include <thread>

std::queue<int> task_queue;
std::mutex queue_mtx;    // 保护队列本身
std::mutex status_mtx;   // 保护队列状态(如是否为空)
bool queue_empty = true;

void add_task(int task) {
    std::lock(queue_mtx, status_mtx);
    std::lock_guard<std::mutex> lk1(queue_mtx, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(status_mtx, std::adopt_lock);
    
    task_queue.push(task);
    queue_empty = false;
}

void get_task() {
    std::lock(queue_mtx, status_mtx);
    std::lock_guard<std::mutex> lk1(queue_mtx, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(status_mtx, std::adopt_lock);
    
    if (!queue_empty) {
        int task = task_queue.front();
        task_queue.pop();
        queue_empty = task_queue.empty();
    }
}

场景 3:资源池的多资源锁定

#include <vector>
#include <mutex>

struct Resource {
    bool in_use;
    std::mutex mtx;
};

std::vector<Resource> resource_pool(10);

// 同时锁定两个资源进行交换操作
void swap_resources(int idx1, int idx2) {
    std::lock(resource_pool[idx1].mtx, resource_pool[idx2].mtx);
    std::lock_guard<std::mutex> lk1(resource_pool[idx1].mtx, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(resource_pool[idx2].mtx, std::adopt_lock);
    
    std::swap(resource_pool[idx1].in_use, resource_pool[idx2].in_use);
}

特点

  • std::lock 比手动按固定顺序加锁稍慢,因有死锁避免的算法开销,但远低于死锁带来的调试和维护成本。

  • 在高并发场景下,若能通过设计避免 “同时锁多个 mutex”,性能会更好;若必须多锁,std::lock 是最安全的选择。

常见坑点

  1. 忘记配合 RAII 锁守卫:std::lock 只负责锁定,不负责解锁!必须用 std::lock_guardstd::unique_lock 配合 std::adopt_lock 标签接管所有权,否则异常时会导致 mutex 永远不被释放。

  2. 混合使用 std::lock 和手动 lock():若部分 mutex 用 std::lock 锁定,部分手动 lock(),会破坏死锁避免的逻辑,仍可能引发死锁。

  3. 锁定非 Lockable 类型:std::lock 只能锁定满足 Lockable 要求的类型如 std::mutexstd::recursive_mutex,不能用于 std::shared_mutex 的独占锁定,需用 std::lock 的变体或 C++17 的 std::scoped_lock

拓展:

C++17 升级:std::scoped_lockstd::lock 的 RAII 封装,支持锁定任意数量的 mutex,用法更简洁:

void transfer(Account& from, Account& to, int amount) {
    std::scoped_lock lock(from.mtx, to.mtx);  // C++17,自动 lock+unlock
    from.balance -= amount;
    to.balance += amount;
}

std::lock 是 C++11 解决多锁死锁的 “安全网”,而 C++17 的 std::scoped_lock 进一步降低了使用门槛。std::scoped_lock 在后文会讲。

QA

std::lock 是如何避免死锁的?

通过死锁避免算法如固定顺序锁定、尝试 - 回退机制,原子性地尝试锁定所有 mutex,失败则释放已锁的并重试,破坏 “循环等待” 条件,保证死锁安全。

std::lock 和 C++17 的 std::scoped_lock 有什么区别?

std::lock 是函数,仅负责锁定,需手动配合 RAII 锁守卫;std::scoped_lock 是类模板,构造时自动调用 std::lock 锁定多个 mutex,析构时自动解锁,代码更简洁安全,推荐优先使用。

使用 std::lock 时为什么必须用 std::adopt_lock

std::lock 已经完成了锁定操作,std::lock_guard/std::unique_lock 默认会在构造时再次调用 lock(),导致未定义行为。std::adopt_lock 标签的作用是告诉锁守卫:“mutex 已经锁好了,直接接管所有权,不要再次锁定”。


mutex + RAII 锁(lock_guard / unique_lock / scoped_lock)

设计原因

手动 lock()/unlock() 极易忘记解锁,或异常时漏解锁 → 死锁。用 RAII资源获取即初始化风格的锁,保证 “构造时 lock,析构时 unlock”,异常安全。

底层原理

  • std::mutex 是操作系统互斥量的封装,同一时间只能有一个线程持有锁;

  • lock_guard 等RAII锁在构造函数中调用 mutex.lock(),析构函数中调用 mutex.unlock(),即使中间抛异常,析构函数也会执行,保证锁被释放。

代码示例:

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

// 危险老写法:手动 lock/unlock
void dangerous_increment() {
    mtx.lock();
    counter++;
    // 如果这里抛异常,永远不 unlock!其他线程会卡死
    mtx.unlock();
}

// 现代写法1:lock_guard(最简单,C++11)
void safe_increment1() {
    {
        std::lock_guard<std::mutex> lg(mtx);  // 构造时 lock
        counter++;  // 临界区
    }  // 作用域结束,lg 析构,自动 unlock
}

// 现代写法2:unique_lock(更灵活,C++11)
void safe_increment2() {
    std::unique_lock<std::mutex> ul(mtx);  // 构造时 lock
    counter++;
    ul.unlock();  // 可以提前 unlock
    // 做其他不需要锁的事
}

// 现代写法3:scoped_lock(多锁防死锁,C++17)
std::mutex mtx1, mtx2;
void multi_lock_safe() {
    std::scoped_lock lock(mtx1, mtx2);  // 内部用 std::lock 算法,按固定顺序加锁,防死锁
    // 同时持有 mtx1 和 mtx2
}  // 自动解锁所有锁

int main() {
    std::thread t1(safe_increment1);
    std::thread t2(safe_increment1);
    t1.join();
    t2.join();
    std::cout << counter << "\n"; // 输出 2
}

C++11基础锁

std::lock_guard(最简单、最常用的 RAII 锁)

设计背景:

C++11 之前,手动管理 mutex.lock()/unlock() 是家常便饭,但极易出错:

  • 忘记 unlock() → 死锁;

  • 中间代码抛异常 → 永远不会执行到 unlock() → 死锁。

std::lock_guard 就是为了解决这个问题设计的,它是一个极简的 RAII 锁管理器,只做一件事 :构造时 lock,析构时 unlock。

std::lock_guard 的源码非常简单,大概长这样:

template <typename Mutex>
class lock_guard {
public:
    // 构造函数:显式要求传入 Mutex 引用,构造时立即 lock
    explicit lock_guard(Mutex& m) : mutex_(m) {
        mutex_.lock();
    }

    // 析构函数:自动 unlock
    ~lock_guard() {
        mutex_.unlock();
    }

    // 禁止拷贝和赋值(锁的所有权不能拷贝)
    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;

private:
    Mutex& mutex_;  // 存的是 Mutex 的引用,不拥有所有权
};

关键点:

  • 它存的是 Mutex&(引用),不拥有 mutex 的所有权,只是帮你管理 lock/unlock;

  • 禁止拷贝和赋值(= delete),因为锁的所有权不能拷贝;

  • 没有默认构造函数,必须传入一个 mutex;

  • 没有提前 unlock 的接口,只能等析构。

详细使用方法

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        // 用法1:直接构造 lock_guard,传入 mutex
        // 注意:必须用 {} 或 () 显式构造,不能写成 std::lock_guard lg = mtx;(因为构造函数是 explicit 的)
        std::lock_guard<std::mutex> lg(mtx);
        counter++;
    }  // 离开作用域,lg 析构,自动 unlock
}

void increment_with_scope() {
    for (int i = 0; i < 10000; ++i) {
        // 用法2:用额外的 {} 缩小临界区范围(推荐!)
        // 临界区越小,锁竞争越少,性能越好
        {
            std::lock_guard<std::mutex> lg(mtx);
            counter++;
        }  // 这里就 unlock 了,后面的代码不需要锁
        // 做其他不需要锁的事
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment_with_scope);
    t1.join();
    t2.join();
    std::cout << "counter: " << counter << "\n";  // 输出 20000
}

适用场景

  • 单锁、简单临界区:只需要一个 mutex,且不需要提前 unlock;

  • 临界区范围明确:用 {} 就能清晰界定临界区;

  • 追求极简:不需要任何额外功能,只要自动 lock/unlock。

特点

  • 零开销:编译器优化后,lock_guard 的代码会被完全内联,和手动 lock()/unlock() 性能一模一样;

  • 没有运行时开销:所有逻辑都在编译期完成。

std::unique_lock(最灵活的 RAII 锁)

设计背景

std::lock_guard 太简单了,有很多局限性:

  • 不能提前 unlock;

  • 不能延迟 lock(构造时必须 lock);

  • 不能配合 std::condition_variable 使用;

  • 不能移动(所有权不能转移)。

为了解决这些问题,C++11 引入了 std::unique_lock,它是一个 “全能型” 的 RAII 锁管理器,提供了几乎所有能想到的锁操作功能。

std::unique_lock 的源码比 lock_guard 复杂很多,核心简化如下:

template <typename Mutex>
class unique_lock {
public:
    // 构造函数1:直接 lock(和 lock_guard 一样)
    explicit unique_lock(Mutex& m) : mutex_(&m), owns_lock_(false) {
        lock();
    }

    // 构造函数2:延迟 lock(std::defer_lock)
    // 构造时不 lock,后面手动 lock
    unique_lock(Mutex& m, std::defer_lock_t) noexcept : mutex_(&m), owns_lock_(false) {}

    // 构造函数3:尝试 lock(std::try_to_lock)
    // 尝试 lock,失败也不阻塞
    unique_lock(Mutex& m, std::try_to_lock_t) : mutex_(&m), owns_lock_(false) {
        try_lock();
    }

    // 构造函数4:已经持有锁(std::adopt_lock)
    // 假设你已经手动 lock 了 mutex,unique_lock 直接接管所有权
    unique_lock(Mutex& m, std::adopt_lock_t) : mutex_(&m), owns_lock_(true) {}

    // 析构函数:如果持有锁,就 unlock
    ~unique_lock() {
        if (owns_lock_) {
            mutex_->unlock();
        }
    }

    // 支持移动!(锁的所有权可以转移)
    unique_lock(unique_lock&& other) noexcept : mutex_(other.mutex_), owns_lock_(other.owns_lock_) {
        other.mutex_ = nullptr;
        other.owns_lock_ = false;
    }

    unique_lock& operator=(unique_lock&& other) noexcept {
        if (this != &other) {
            if (owns_lock_) {
                mutex_->unlock();
            }
            mutex_ = other.mutex_;
            owns_lock_ = other.owns_lock_;
            other.mutex_ = nullptr;
            other.owns_lock_ = false;
        }
        return *this;
    }

    // 禁止拷贝
    unique_lock(const unique_lock&) = delete;
    unique_lock& operator=(const unique_lock&) = delete;

    // 核心操作接口
    void lock() {
        mutex_->lock();
        owns_lock_ = true;
    }

    bool try_lock() {
        owns_lock_ = mutex_->try_lock();
        return owns_lock_;
    }

    void unlock() {
        if (owns_lock_) {
            mutex_->unlock();
            owns_lock_ = false;
        }
    }

    // 检查是否持有锁
    bool owns_lock() const noexcept { return owns_lock_; }
    explicit operator bool() const noexcept { return owns_lock_; }

    // 释放所有权(返回 mutex 指针,不再管理锁)
    Mutex* release() noexcept {
        Mutex* m = mutex_;
        mutex_ = nullptr;
        owns_lock_ = false;
        return m;
    }

    // 获取 mutex 指针
    Mutex* mutex() const noexcept { return mutex_; }

private:
    Mutex* mutex_;      // 存的是指针,支持 nullptr(移动后)
    bool owns_lock_;    // 标记是否持有锁
};

关键点:

  • 存的是 Mutex*(指针),不是引用,支持 nullptr(移动后原对象的指针置空);

  • owns_lock_ 标记,记录是否持有锁;

  • 支持移动(&&),锁的所有权可以转移;

  • 提供了多种构造策略(defer_locktry_to_lockadopt_lock);

  • 提供了 lock()try_lock()unlock() 等手动操作接口。

详细使用方法

场景 1:基本用法(和 lock_guard 一样)

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

void basic_usage() {
    for (int i = 0; i < 10000; ++i) {
        std::unique_lock<std::mutex> ul(mtx);  // 构造时 lock
        counter++;
    }  // 析构时 unlock
}

场景 2:延迟 lock(std::defer_lock),构造时不 lock,后面手动 lock,适用于 “需要先做一些准备工作,再进入临界区” 的场景:

void defer_lock_usage() {
    std::unique_lock<std::mutex> ul(mtx, std::defer_lock);  // 构造时不 lock
    
    // 做一些不需要锁的准备工作
    int temp = 0;
    for (int i = 0; i < 100; ++i) temp++;
    
    ul.lock();  // 准备工作做完了,手动 lock
    counter += temp;
    ul.unlock();  // 可以提前 unlock
    
    // 做其他不需要锁的事
}

场景 3:尝试 lock(std::try_to_lock),如果失败,锁被其他线程持有,也不阻塞,直接返回,适用于 “如果拿不到锁就做别的事” 的场景:

void try_lock_usage() {
    for (int i = 0; i < 10000; ++i) {
        std::unique_lock<std::mutex> ul(mtx, std::try_to_lock);  // 尝试 lock
        if (ul.owns_lock()) {  // 检查是否拿到锁
            counter++;
        } else {
            // 没拿到锁,做别的事(比如打印日志、跳过这次循环)
            std::cout << "没拿到锁,跳过\n";
        }
    }
}

场景 4:接管已有的锁(std::adopt_lock),假设你已经手动 lock 了 mutex,让 unique_lock 接管所有权,适用于 “需要和旧代码配合” 的场景:

void adopt_lock_usage() {
    mtx.lock();  // 手动 lock(旧代码遗留)
    // 让 unique_lock 接管,后面不用手动 unlock 了
    std::unique_lock<std::mutex> ul(mtx, std::adopt_lock);
    counter++;
}  // ul 析构,自动 unlock

场景 5:提前 unlock,临界区结束后,提前 unlock,减少锁的持有时间,提高并发性能:

void early_unlock_usage() {
    std::unique_lock<std::mutex> ul(mtx);
    counter++;
    ul.unlock();  // 提前 unlock,后面的代码不需要锁
    
    // 做耗时操作(比如 IO、复杂计算),不持有锁
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

场景 6:配合 condition_variable 使用,std::condition_variablewait() 函数必须接受 std::unique_lock,因为它需要在等待时原子地 unlock mutex,唤醒后重新 lock:

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;

void producer() {
    for (int i = 0; i < 5; ++i) {
        {
            std::unique_lock<std::mutex> ul(mtx);
            q.push(i);
            std::cout << "生产:" << i << "\n";
        }
        cv.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    while (true) {
        // wait() 必须用 unique_lock!
        std::unique_lock<std::mutex> ul(mtx);
        cv.wait(ul, []{ return !q.empty(); });  // 原子地 unlock + 等待
        
        int val = q.front();
        q.pop();
        std::cout << "消费:" << val << "\n";
        
        if (val == 4) break;
    }
}

int main() {
    std::thread t_prod(producer);
    std::thread t_cons(consumer);
    t_prod.join();
    t_cons.join();
}

场景 7:锁的所有权转移(移动语义),unique_lock 支持移动,锁的所有权可以从一个对象转移到另一个对象,适用于 “需要把锁传给函数” 的场景:

#include <iostream>
#include <mutex>
#include <utility>  // std::move

std::mutex mtx;

void process_with_lock(std::unique_lock<std::mutex> ul) {
    // 这里持有锁
    std::cout << "处理数据...\n";
}  // ul 析构,自动 unlock

void move_usage() {
    std::unique_lock<std::mutex> ul(mtx);  // 持有锁
    // 把锁的所有权转移给 process_with_lock 函数
    process_with_lock(std::move(ul));
    // 现在 ul 不再持有锁,mutex_ 是 nullptr
}

适用场景

  • 需要灵活控制锁的生命周期:提前 unlock、延迟 lock;

  • 配合 condition_variable 使用:必须用 unique_lock

  • 需要尝试 lock:try_to_lock

  • 需要转移锁的所有权:移动语义;

  • 需要和旧代码配合:adopt_lock

特点

  • 比 lock_guard 略重,因为有 owns_lock_ 标记,需要多存一个 bool,且析构时需要检查 owns_lock_;但编译器优化后差异极小:在大多数场景下,性能和 lock_guard 几乎一样;

  • 灵活性的代价是轻微的空间开销:每个 unique_lock 对象比 lock_guard 多占一个指针和一个 bool 的空间(在 64 位系统上,多 16 字节)。

std::adopt_lock

std::lock_guardstd::unique_lock 是 C++ 标准库提供的 RAII 锁守卫,默认行为是 “构造时自动 lock,析构时自动 unlock”。但在以下两种场景中,我们需要先手动 lock mutex,再交给 RAII 守卫管理:

  1. 使用 std::lock 同时锁定多个 mutex 以避免死锁,此时 mutex 已被 std::lock 锁好。

  2. 需要在 RAII 守卫构造前执行一些前置逻辑,再手动 lock mutex。

如果此时直接用默认构造的 RAII 守卫,会导致重复 lock,普通 std::mutex 不允许同一线程连续 lock 两次,会引发未定义行为或死锁。

C++11 引入 std::adopt_lock 标签常量,专门用于告知 RAII 守卫:“mutex 已经锁好了,直接接管所有权,不要再次 lock”。

std::adopt_lock是 C++ 标准库定义的一个空的标签常量,可以理解为一个 “信物” 或 “口令”。

它唯一的作用:传给 std::lock_guardstd::unique_lock,告诉它们:

“这个 mutex 我已经提前锁好了!你构造的时候千万不要再锁一次,只需要在析构的时候帮我解锁就行了。”

代码示例:

std::mutex mtx;

void good_func() {
    mtx.lock(); // 我们手动先把锁锁上了
    
    // ✅ 用 std::adopt_lock 打个招呼
    std::lock_guard<std::mutex> lock(mtx, std::adopt_lock); 
    // lock_guard 收到信号:“哦,已经锁好了是吧?行,那我不管锁了,只负责等下帮你解锁。”
    
    // ... 临界区代码 ...
    
} // 函数结束,lock_guard 析构,自动调用 mtx.unlock()。完美!

常见坑

在未 lock 的 mutex 上使用 std::adopt_lock

std::mutex mtx;
void bad_func() {
    // 错误!mtx 还没 lock,直接用 adopt_lock
    std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
    // 析构时会对未 lock 的 mutex 执行 unlock,引发未定义行为!
}

与 std::defer_lock 混淆:

  • std::adopt_lock:mutex 已 lock,接管后不 lock,析构时 unlock。

  • std::defer_lock:mutex 未 lock,接管后也不 lock,之后需手动调用 lock()

  • 两者功能完全相反,切勿用错。

QA:

lock_guard 和 unique_lock 区别?

  • lock_guard 最简单,构造时 lock,析构时 unlock,不可提前解锁,不可移动。零开销抽象。

  • unique_lock 更灵活,支持延迟 lock(构造时不 lock,后面手动 lock)、提前 unlock、重锁、timed lock(try_lock_for),可移动,且能配合 condition_variable 使用(wait() 需要 unique_lock)。稍微有一点点开销,因为它要记录当前是否持有锁。

std::adopt_lock 的作用是什么?

它是一个标签常量,用于传递给 std::lock_guardstd::unique_lock,告知它们 “mutex 已经被当前线程 lock 好了,不要在构造时重复 lock,只需在析构时 unlock”。

std::adopt_lock 最常与哪个函数配合使用?为什么?

最常与 std::lock 配合。因为 std::lock 用于同时锁定多个 mutex 以避免死锁,执行完 std::lock 后 mutex 已被锁好,需要用 std::adopt_lock 让 RAII 守卫接管,保证异常安全。

使用 std::adopt_lock 时如果 mutex 未被 lock 会怎样?

会导致未定义行为。RAII 守卫析构时会对未 lock 的 mutex 执行 unlock(),这违反了 std::mutex 的使用规范,可能引发程序崩溃或死锁。

C++14读写锁

std::shared_lock(读写锁的读锁)

设计背景

在很多场景下,我们有读多写少的需求:

  • 多个线程可以同时读数据,互不阻塞;

  • 但写数据时,必须独占访问,所有读线程和其他写线程都要阻塞。

C++11 没有提供标准的读写锁,C++14 才引入了 std::shared_mutex(读写锁本身)和 std::shared_lock(读写锁的读锁 RAII 管理器)。

读写锁的工作原理

std::shared_mutex 支持两种锁模式:

  1. 共享锁(Shared Lock):多个线程可以同时持有,用于读操作;

  2. 独占锁(Exclusive Lock):同一时间只能有一个线程持有,用于写操作。

规则:

  • 一个线程持有共享锁时,其他线程可以继续加共享锁,但不能加独占锁;

  • 一个线程持有独占锁时,其他线程既不能加共享锁,也不能加独占锁;

  • 写操作优先还是读操作优先,取决于具体实现(通常是写优先,避免写饥饿)。

std::shared_lock 的源码和 std::unique_lock 非常像,只是它调用的是 mutex.lock_shared()mutex.unlock_shared()

template <typename Mutex>
class shared_lock {
public:
    // 构造函数:加共享锁
    explicit shared_lock(Mutex& m) : mutex_(&m), owns_lock_(false) {
        lock();
    }

    // 析构函数:解共享锁
    ~shared_lock() {
        if (owns_lock_) {
            mutex_->unlock_shared();
        }
    }

    // 支持移动,接口和 unique_lock 几乎一样
    shared_lock(shared_lock&& other) noexcept;
    shared_lock& operator=(shared_lock&& other) noexcept;

    // 核心操作:共享锁
    void lock() {
        mutex_->lock_shared();
        owns_lock_ = true;
    }

    void unlock() {
        if (owns_lock_) {
            mutex_->unlock_shared();
            owns_lock_ = false;
        }
    }

    // 其他接口和 unique_lock 一样:owns_lock(), release(), mutex() 等

private:
    Mutex* mutex_;
    bool owns_lock_;
};

详细使用方法

生产者 - 消费者 - 读写锁例子

#include <iostream>
#include <shared_mutex>  // C++14 必须包含
#include <thread>
#include <vector>

std::shared_mutex rw_mtx;
int data = 0;  // 共享数据

// 读线程:用 shared_lock,多个读可以同时进行
void read_data(int thread_id) {
    for (int i = 0; i < 3; ++i) {
        {
            std::shared_lock<std::shared_mutex> sl(rw_mtx);  // 加共享锁
            std::cout << "线程 " << thread_id << " 读数据:" << data << "\n";
        }  // 解共享锁
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

// 写线程:用 unique_lock(或 lock_guard),独占访问
void write_data(int val) {
    for (int i = 0; i < 2; ++i) {
        {
            std::unique_lock<std::shared_mutex> ul(rw_mtx);  // 加独占锁
            data = val;
            std::cout << "写数据:" << data << "\n";
        }  // 解独占锁
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    // 启动 3 个读线程
    std::vector<std::thread> readers;
    for (int i = 0; i < 3; ++i) {
        readers.emplace_back(read_data, i);
    }

    // 启动 1 个写线程
    std::thread writer(write_data, 42);

    // 等待所有线程结束
    for (auto& t : readers) {
        t.join();
    }
    writer.join();
}

输出示例

线程 0 读数据:0
线程 1 读数据:0
线程 2 读数据:0
写数据:42
线程 0 读数据:42
线程 1 读数据:42
线程 2 读数据:42
写数据:42
线程 0 读数据:42
线程 1 读数据:42
线程 2 读数据:42

适用场景

  • 读多写少,读操作远多于写操作,比如配置文件读取、缓存数据访问;

  • 需要高并发读,多个读线程可以同时访问,提高性能。

特点

  • 读多写少场景下性能远优于普通 mutex,多个读线程可以同时进行,没有锁竞争;

  • 写操作开销比普通 mutex 略大,因为需要管理共享锁和独占锁的状态;

  • 不适合写多读少的场景,如果写操作很多,读写锁的优势会消失,甚至比普通 mutex 更慢。

C++17 多锁神器

std::scoped_lock(多锁防死锁)

设计背景

当需要同时持有多个 mutex 时,死锁是最常见的问题:

线程 A 先 lock mtx1,再 lock mtx2;线程 B 先 lock mtx2,再 lock mtx1;结果A 持有 mtx1,等待 mtx2;B 持有 mtx2,等待 mtx1 → 死锁。

 死锁的经典例子:

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx1, mtx2;

void thread_a() {
    for (int i = 0; i < 10000; ++i) {
        mtx1.lock();  // 先 lock mtx1
        mtx2.lock();  // 再 lock mtx2
        // 临界区
        mtx2.unlock();
        mtx1.unlock();
    }
}

void thread_b() {
    for (int i = 0; i < 10000; ++i) {
        mtx2.lock();  // 先 lock mtx2
        mtx1.lock();  // 再 lock mtx1
        // 临界区
        mtx1.unlock();
        mtx2.unlock();
    }
}

int main() {
    std::thread t1(thread_a);
    std::thread t2(thread_b);
    t1.join();
    t2.join();
    std::cout << "完成\n";  // 大概率不会输出,程序死锁了!
}

C++11 提供了 std::lock(m1, m2, ...) 算法,可以按固定顺序同时加多个锁,防死锁,但它不是 RAII 的,需要手动 unlock。

C++17 引入了 std::scoped_lock—— 它是一个支持多锁的 RAII 锁管理器,内部使用 std::lock 算法,自动按固定顺序加锁,自动解锁,彻底解决多锁死锁问题。

std::scoped_lock 的核心是可变参数模板和 std::lock 算法,简化如下:

// 基础版本:单锁(和 lock_guard 一样)
template <typename... MutexTypes>
class scoped_lock;

template <typename Mutex>
class scoped_lock<Mutex> {
public:
    explicit scoped_lock(Mutex& m) : mutex_(m) {
        mutex_.lock();
    }

    ~scoped_lock() {
        mutex_.unlock();
    }

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

private:
    Mutex& mutex_;
};

// 多锁版本:用 std::lock 算法按固定顺序加锁
template <typename... Mutexes>
class scoped_lock {
public:
    explicit scoped_lock(Mutexes&... m) : mutexes_(m...) {
        // 核心:用 std::lock 同时加多个锁,内部按固定顺序,防死锁
        std::lock(m...);
    }

    ~scoped_lock() {
        // 按构造的反顺序解锁(C++17 标准规定)
        std::apply([](auto&... m) { (..., m.unlock()); }, mutexes_);
    }

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

private:
    std::tuple<Mutexes&...> mutexes_;  // 用 tuple 存多个 mutex 的引用
};

关键点:

  • 支持可变参数(...),可以同时管理任意数量的 mutex;

  • 内部使用 std::lock(m...) 算法,按固定顺序加锁,防死锁;

  • 析构时按构造的反顺序解锁;

  • 禁止拷贝和移动;

  • 单锁时和 lock_guard 一样,多锁时自动防死锁。

详细使用方法:

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx1, mtx2;

void thread_a() {
    for (int i = 0; i < 10000; ++i) {
        // C++17:用 scoped_lock 同时管理 mtx1 和 mtx2
        std::scoped_lock lock(mtx1, mtx2);  // 自动按固定顺序加锁,防死锁
        // 临界区
    }  // 自动解锁
}

void thread_b() {
    for (int i = 0; i < 10000; ++i) {
        // 即使这里顺序和 thread_a 不一样,也不会死锁!
        // 因为 scoped_lock 内部会按固定顺序加锁
        std::scoped_lock lock(mtx2, mtx1);
        // 临界区
    }
}

int main() {
    std::thread t1(thread_a);
    std::thread t2(thread_b);
    t1.join();
    t2.join();
    std::cout << "完成\n";  // 一定会输出,不会死锁!
}

适用场景

  • 需要同时持有多个 mutex:这是 scoped_lock 的唯一设计目标;

  • C++17 及以上,替代 C++11 的 std::lock + 手动 unlock,更安全。

特点

  • 和手动用 std::lock 一样:编译器优化后零开销;

  • 多锁时的固定顺序开销极小:std::lock 算法的开销可以忽略不计。

四个锁的详细对比表

引入版本特点支持的操作适用场景性能
lock_guardC++11极简,构造时 lock,析构时 unlock,不可提前 unlock,不可移动仅自动 lock/unlock单锁、简单临界区零开销
unique_lockC++11最灵活,支持延迟 lock、提前 unlock、try_lock、移动、配合 condition_variablelock, unlock, try_lock, defer_lock, adopt_lock, move需要灵活控制锁、配合 condition_variable略高于 lock_guard,优化后几乎一样
shared_lockC++14读写锁的读锁,多个线程可同时持有lock_shared, unlock_shared读多写少的读操作读时无竞争,写时略重
scoped_lockC++17多锁 RAII,自动防死锁,支持任意数量 mutex同时 lock 多个 mutex,自动 unlock需要同时持有多个 mutex零开销

选择指南

  1. 单锁、简单临界区 → 优先用 lock_guard,最简单、零开销;

  2. 需要灵活控制锁、配合 condition_variable → 用 unique_lock

  3. 读多写少 → 读操作用 shared_lock,写操作用 unique_lock,配合 std::shared_mutex

  4. 需要同时持有多个 mutex → 用 scoped_lock(C++17)或 std::lock + lock_guard(C++11);

  5. C++17 及以上 → 尽量用 scoped_lock 替代 lock_guard(单锁时一样,多锁时更安全)。

性能优化

死锁的四个必要条件,只要打破其中一个,就能避免死锁:

  1. 互斥条件:一个资源同一时间只能被一个线程持有(锁的本质,无法打破);

  2. 请求与保持条件:线程持有一个资源,同时请求另一个资源(打破方法:一次性请求所有资源);

  3. 不可剥夺条件:资源不能被强行剥夺,只能由持有者主动释放(锁的本质,无法打破);

  4. 循环等待条件:线程之间形成循环等待链(打破方法:按固定顺序加锁)。

避免死锁的具体方法

  1. 按固定顺序加锁:所有线程都按相同的顺序加锁,比如按 mutex 的地址顺序、或按名字的字典序;

  2. std::scoped_lock(C++17)或 std::lock(C++11):内部已经按固定顺序加锁;

  3. 避免嵌套锁:一个线程已经持有锁时,不要再申请其他锁;

  4. 用超时锁:unique_lock 支持 try_lock_for(),如果拿不到锁就放弃,避免无限等待;

  5. 尽量减少锁的持有时间:临界区越小越好,锁竞争越少。

锁的性能优化建议

  1. 尽量缩小临界区:只把真正需要保护的代码放在临界区里,其他代码移到外面;

  2. 避免在临界区里做耗时操作:比如 IO、复杂计算、睡眠等;

  3. 读多写少用读写锁:std::shared_mutex + std::shared_lock

  4. 优先用无锁编程:高性能场景下,用 std::atomic 替代 mutex;

  5. 用线程池减少线程创建开销:频繁创建和销毁线程的开销很大,用线程池复用线程;

  6. 避免锁的伪共享:多个原子变量或 mutex 不要放在同一个缓存行里(64 字节),否则会导致缓存颠簸。


condition_variable(条件变量)

设计原因

线程间 “等待某个条件成立” 时,不能一直轮询浪费 CPU,需要高效的通知机制:线程 A 等待条件,线程 B 满足条件后通知 A 醒来。

底层原理

配合 std::mutex 使用:

  • wait() 会原子地 unlock mutex + 让线程睡眠,直到被 notify_one()/notify_all() 唤醒;

  • 唤醒后会重新 lock mutex,检查条件是否成立,防止虚假唤醒。

实际例子

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;  // 共享队列

// 生产者:往队列里放数据
void producer() {
    for (int i = 0; i < 5; ++i) {
        {
            std::lock_guard<std::mutex> lg(mtx);
            q.push(i);
            std::cout << "生产:" << i << "\n";
        }
        cv.notify_one();  // 通知一个消费者
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟生产耗时
    }
}

// 消费者:从队列里取数据
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> ul(mtx);
        // wait 的第二个参数是“谓词”,防止虚假唤醒:只有当 !q.empty() 时才真的醒来
        cv.wait(ul, []{ return !q.empty(); });
        
        int val = q.front();
        q.pop();
        std::cout << "消费:" << val << "\n";
        
        if (val == 4) break;  // 消费完最后一个数据退出
    }
}

int main() {
    std::thread t_prod(producer);
    std::thread t_cons(consumer);
    t_prod.join();
    t_cons.join();
}

为什么需要谓词?

即使没有调用 notify_one(),线程也可能被操作系统 “虚假唤醒”。如果没有谓词,线程醒来后会直接执行后续代码,此时条件可能不成立,比如队列还是空的,导致 bug。谓词的作用是 “醒来后再检查一遍条件,不成立就继续睡”。


async /future/promise /packaged_task

设计原因

不想手动管理线程的创建、join、返回值和异常传播时,用 “任务” 方式写并发,更简单、更安全。这是现代 C++ 推荐的默认并发方式。

核心组件关系

  • std::async:最常用的高级接口,自动选择线程池或新线程执行任务,返回 std::future

  • std::future:代表 “未来会拿到的结果”,调用 get() 会阻塞等待结果,并自动传播任务抛出的异常;

  • std::promise:手动设置结果或异常的 “生产者”,配合 future 使用;

  • std::packaged_task:把函数包装成可异步执行的任务,内部关联 future。

关系图:

  • Promise 是结果的输入端(Set)。

  • Future 是结果的输出端(Get)。

  • Packaged Task 内部包含了一个 Promise 和一个函数。

  • Async 内部帮你创建好了 Packaged Task 和 Thread。

简单例子:

 实际例子:

例子 1:std::async,适用于 “我有一个函数,想让它在后台跑,一会来拿结果”。

#include <iostream>
#include <future>  
#include <chrono>
#include <stdexcept>

// 模拟耗时计算,可能抛异常
int heavy_computation(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    if (x < 0) throw std::runtime_error("Negative number!");
    return x * x;
}

int main() {
    // 关键点:显式指定 std::launch::async 强制创建新线程
    // 如果不指定,编译器可能会选择 deferred(懒执行,不并发)
    auto fut = std::async(std::launch::async, heavy_computation, 10);
    
    std::cout << "主线程做其他事...\n";
    
    try {
        // get() 阻塞等待
        // 如果任务抛异常,这里会自动 rethrow,无需手动处理错误码
        int result = fut.get();  
        std::cout << "结果:" << result << "\n";
    } catch (const std::exception& e) {
        std::cout << "捕获到异常:" << e.what() << "\n";
    }
    
    // 注意:future 是 RAII 的,不需要手动清理
    return 0;
}

例子 2:std::promise,适用于 “结果由另一个线程手动设置” 的场景,比如结果不是由一个简单的函数返回,而是来自事件循环、回调或者复杂的流程控制。

#include <iostream>
#include <future>
#include <thread>

// promise 是 move-only 的,必须按值传递(move 进去)
void do_work(std::promise<int> p) {
    try {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        // 手动设置值
        p.set_value(42); 
    } catch (...) {
        // 也可以手动设置异常
        p.set_exception(std::current_exception());
    }
}

int main() {
    // 1. 创建一个 Promise 对象
    std::promise<int> p;
    
    // 2. 从 Promise 获取与其关联的 Future
    std::future<int> fut = p.get_future();
    
    // 3. 将 Promise 移动到新线程中去执行逻辑
    std::thread t(do_work, std::move(p)); // promise 不能拷贝,必须 move
    
    std::cout << "等待结果...\n";
    
    // 4. 主线程通过 Future 取值
    int result = fut.get();
    std::cout << "结果:" << result << "\n";
    
    t.join();
    return 0;
}

例子 3:std::packaged_task,适用于 “把函数包装成任务,交给其他线程执行” 的场景。它是连接 “函数对象” 和 “future/promise” 的桥梁。常用于构建线程池。

#include <iostream>
#include <future>
#include <thread>
#include <vector>

int add(int a, int b) { return a + b; }

int main() {
    // 1. 包装函数
    // 模板参数是函数签名 int(int, int)
    std::packaged_task<int(int, int)> task(add); 
    
    // 2. 获取 future
    std::future<int> fut = task.get_future();
    
    // 3. 把任务交给线程执行
    // packaged_task 也是 move-only 的
    std::thread t(std::move(task), 1, 2); 
    
    // 4. 获取结果
    int result = fut.get();
    std::cout << "结果:" << result << "\n"; // 输出 3
    
    t.join();
    
    // 进阶:packaged_task 可以像普通函数一样被调用,不一定非要在新线程
    std::packaged_task<int(int, int)> task2(add);
    auto fut2 = task2.get_future();
    task2(10, 20); // 在当前线程直接调用
    std::cout << "直接调用结果:" << fut2.get() << "\n";
    
    return 0;
}

为了方便理解,我们把这四个组件对应成生活场景:

C++ 组件奶茶店角色作用

std::promise

奶茶店店员

负责生产结果(做奶茶)。做好了之后,把结果放在一个地方。

std::future

取餐号小票

你手里的凭证。你可以拿着它去等待结果(奶茶做好了吗?),好了就取走结果。

std::packaged_task

奶茶制作订单

它把 “做奶茶的流程(函数)” 和 “小票(future)” 绑定在一起。

std::async

VIP 一键下单服务

最省心。你只要说 “我要一杯奶茶”,它自动帮你搞定:创建订单、找店员做、把小票给你。

核心关系:

  • Promise 是输入端(Set):负责往里写结果。

  • Future 是输出端(Get):负责往外读结果。

  • 它们是配对使用的,底层通过一个共享状态(Shared State)连接。

[你的线程]                      [新线程/任务线程]
    |                                 |
    |  std::future                    |  std::promise / std::packaged_task
    |  (消费者)                       |  (生产者)
    |                                 |
    |  fut.get()  <--(等待结果)-->   p.set_value()
    |  (阻塞)                         (设置结果)
    |                                 |
    |  拿到结果了!                    |

从最底层的 std::promise 开始讲,最后到最方便的 std::async

例子 1:std::promise & std::future(底层机制,手动控制),这是最底层的机制。

场景:你去饭店吃饭,你在座位上等(主线程),厨师在厨房做(子线程)

#include <iostream>
#include <future>   // 必须包含这个头文件
#include <thread>
#include <chrono>

// 这是一个在子线程运行的函数
// 注意参数:std::promise<int> 是按值传递的,而且必须用 std::move 转移所有权
void chef(std::promise<int> dish_promise) {
    std::cout << "厨师:开始做菜...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时
    
    // 关键点 1:设置结果
    // promise 负责在一端写入数据
    dish_promise.set_value(42); 
    
    std::cout << "厨师:菜做好了!\n";
}

int main() {
    // 1. 创建一个 Promise 对象(生产者)
    // 模板参数是结果的类型 int
    std::promise<int> prom;
    
    // 2. 从 Promise 中获取与其关联的 Future 对象(消费者)
    // future 是 promise 的“好兄弟”,promise 负责写,future 负责读
    std::future<int> fut = prom.get_future();
    
    std::cout << "顾客:点了菜,拿到了取餐号,去玩手机了...\n";
    
    // 3. 启动线程,把 promise 移动给子线程
    // 注意:std::promise 是 move-only 的(不能拷贝),必须用 std::move
    std::thread t(chef, std::move(prom));
    
    // 4. 主线程做自己的事...
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "顾客:玩完手机了,看看菜做好没...\n";
    
    // 关键点 2:获取结果
    // future.get() 会阻塞当前线程,直到 promise 设置了值
    // 如果 promise 已经设好了值,get() 会立刻返回
    int result = fut.get();
    
    std::cout << "顾客:拿到菜了!结果是:" << result << "\n";
    
    t.join(); // 记得回收线程
    return 0;
}

进阶:如果任务抛异常了怎么办?

std::promise 也可以设置异常,future.get() 会自动把异常抛出来:

#include <stdexcept>

void may_throw(std::promise<int> p) {
    try {
        throw std::runtime_error("厨房着火了!");
        p.set_value(42);
    } catch (...) {
        // 捕获任何异常,通过 set_exception 传给 future
        p.set_exception(std::current_exception());
    }
}

// main 函数里:
// int result = fut.get(); // 这里会抛出 "厨房着火了" 的异常

例子 2:std::packaged_task

std::promise 太底层了,每次都要手动调用 set_valuestd::packaged_task 是一个封装器。它把一个普通函数或可调用对象包起来,当这个函数执行完,它会自动把返回值塞进 Promise 里。

场景:你有一个加法函数,你想把它变成一个异步任务

#include <iostream>
#include <future>
#include <thread>

// 一个普通函数
int add(int a, int b) {
    std::cout << "线程:正在计算 " << a << " + " << b << "...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return a + b;
}

int main() {
    // 1. 创建 packaged_task
    // 模板参数是函数签名 int(int, int)
    // 它内部自动包含了一个 promise 和一个函数
    std::packaged_task<int(int, int)> task(add);
    
    // 2. 依然是获取 future
    std::future<int> fut = task.get_future();
    
    std::cout << "主线程:任务已打包,准备执行...\n";
    
    // 3. 执行任务
    // 方式 A:像普通函数一样在当前线程调用(没意义,演示用)
    // task(10, 20); 
    
    // 方式 B:移动到新线程去执行(这才是并发)
    // packaged_task 也是 move-only 的
    std::thread t(std::move(task), 10, 20);
    
    // 4. 获取结果
    std::cout << "主线程:等待结果...\n";
    int result = fut.get();
    std::cout << "主线程:结果是 " << result << "\n";
    
    t.join();
    return 0;
}

总结 packaged_task:它就是把 “函数调用” 变成了 “异步任务”,不用手动 set_value 了,函数 return 什么,它自动帮你 set 什么。

例子 3:std::async,这是最简单、最推荐使用的接口。它把 packaged_taskthread 都封装好了,你甚至看不到线程的存在。

场景:我就是想异步算个东西,别让我写那么多废话。

#include <iostream>
#include <future>
#include <chrono>

int long_task(int x) {
    std::cout << "任务线程:开始工作...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return x * x;
}

int main() {
    std::cout << "主线程:发起异步任务...\n";
    
    // 关键点:std::async
    // 参数 1:启动策略(后面详解)
    // 参数 2:函数名
    // 参数 3...:函数参数
    // 返回值:std::future
    auto fut = std::async(std::launch::async, long_task, 10);
    
    std::cout << "主线程:我可以先去干点别的...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    std::cout << "主线程:好了,我要拿结果了...\n";
    
    // get() 依然是阻塞等待
    // 如果 long_task 抛异常,这里会自动捕获并抛出
    int result = fut.get();
    
    std::cout << "主线程:结果是 " << result << "\n";
    
    // 注意:不需要 join 任何线程!async 自动管理生命周期
    return 0;
}

std::async 的三种启动策略,调用 std::async 时,第一个参数决定了它到底会不会创建新线程。

策略行为注意事项
std::launch::async立刻启动新线程执行任务保证并发。
std::launch::deferred懒执行:调用 future.get() 时才在当前线程执行任务不并发。任务会推迟到 get () 那一刻,且在调用者线程跑。
默认(不指定)实现可以选择:可能是 async,也可能是 deferred常见坑:MSVC 和 GCC 的实现策略不同。如果不指定,任务可能不会并行执行!建议永远显式指定策略

如果你写 std::async(func) 而不指定策略,编译器通常会选择 deferred。这意味着你的代码根本没有并发!它只是把函数推迟到 get() 那一刻在主线程跑了一圈。

因此,建议永远显式指定 std::launch::async,除非你明确知道自己想要延迟执行

// 推荐写法(强制并发)
auto fut = std::async(std::launch::async, func);

// 不推荐写法(可能根本不并发)
// auto fut = std::async(func); 

std::shared_future(让多个线程等待同一个结果)

std::future 有个限制:它是 move-only 的,而且只能 get() 一次。如果你想让多个线程都等待同一个结果,就需要 std::shared_future

#include <iostream>
#include <future>
#include <thread>

int get_data() { return 42; }

void worker(std::shared_future<int> sf) {
    // 多个线程都可以调用 sf.get()
    std::cout << "线程 " << std::this_thread::get_id() << " 拿到数据:" << sf.get() << "\n";
}

int main() {
    std::future<int> fut = std::async(std::launch::async, get_data);
    
    // 转换为 shared_future,可以拷贝给多个线程
    std::shared_future<int> shared_fut = fut.share();
    
    std::thread t1(worker, shared_fut);
    std::thread t2(worker, shared_fut);
    
    t1.join();
    t2.join();
    return 0;
}

常见坑:

  • future.get () 调用两次 → 第二次崩溃,future 是 “一次性” 的,只能 get 一次,因为它会转移结果的所有权;

  • std::async 默认策略 → 可能是 deferred,任务不会并行执行,建议显式指定 std::launch::async

QA:

std::async 和 std::thread 区别?什么时候用哪个?

  • 区别:

    • std::async 是高级抽象,自动管理线程(可能用线程池),返回 future 自动获取结果和传播异常。

    • std::thread 是底层接口:需要手动 join/detach,手动处理返回值和异常。

  • 使用场景:

    • 中小项目、简单任务:优先用 std::async,更简单安全。

    • 需要精确控制线程生命周期、或线程需要长期运行比如一个常驻的 IO 线程,用 std::thread


std::atomic+无锁编程

设计原因

高性能场景下,mutex 的锁竞争开销太大(上下文切换、内核态切换),用 CPU 原子指令如 compare-and-swap, CAS实现无锁编程,性能更高。锁能保证正确性,但在高并发、高频竞争的场景下,锁的开销是不可接受的。我们需要一种机制,既能保证 “读 - 改 - 写” 操作的原子性,又不经过内核、不阻塞线程。这就是 std::atomic 和 无锁编程。

底层原理

  1. CPU 指令级支持:利用 CPU 提供的原子指令如 LOCK 前缀在 x86 上,保证操作 “要么全做,要么不做”,不会被其他线程打断。

  2. 内存可见性:编译器保证内存可见性,通过 memory_order 控制一个线程对原子变量的修改,其他线程能及时看到。这涉及到禁止编译器乱序重排和CPU 内存屏障。

现代 CPU 提供了专门的原子指令。

  • x86/x64 架构:LOCK 指令前缀。

  • ARM 架构:LDREX / STREX 指令。

这些指令能保证:在执行 “读 - 改 - 写” 操作时,内存总线被锁住,其他 CPU 核无法访问这块内存,从而保证了操作的原子性。

std::atomic 基本用法

C++11 提供了模板类 std::atomic<T>,让我们可以把任意类型 T(通常是整型、指针)变成原子类型。

用 std::atomic 改写计数器

#include <iostream>
#include <thread>
#include <vector>
#include <atomic> // 必须包含

// 定义一个原子计数器
// 注意:std::atomic 是模板,这里 T 是 int
std::atomic<int> counter{0}; 

void increment() {
    for (int i = 0; i < 100000; ++i) {
        // 原子自增操作
        // 这行代码在 CPU 层面是一条原子指令,不会被打断
        counter.fetch_add(1, std::memory_order_relaxed); 
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }
    for (auto& t : threads) t.join();
    
    // 结果:永远是 1,000,000!
    std::cout << "Atomic Counter: " << counter << "\n"; 
    return 0;
}

std::atomic 常用 API 详解

std::atomic 提供了丰富的操作。我们以最常用的 std::atomic<int> 为例。

1. 基础读写操作

std::atomic<int> a{0};

// 写操作(Store)
a.store(10, std::memory_order_relaxed); // 原子地把 a 设为 10

// 读操作(Load)
int val = a.load(std::memory_order_relaxed); // 原子地读取 a 的值

2. 算术运算,这些函数返回的是修改前的值

std::atomic<int> a{10};

// fetch_add:原子加
int old_val = a.fetch_add(5); // old_val = 10, a 变成 15

// fetch_sub:原子减
a.fetch_sub(3); // a 变成 12

// 位运算也支持
a.fetch_and(0xFF); // 原子与
a.fetch_or(0x01);  // 原子或
a.fetch_xor(0x01); // 原子异或

3. 交换操作

std::atomic<int> a{10};

// exchange:原子地把 a 设为 20,并返回 a 之前的值
int old_val = a.exchange(20); // old_val = 10, a 变成 20

4. 最强大的操作:Compare-and-Swap (CAS),这是无锁编程的基石。它的逻辑是:

“如果当前值等于预期值,就把它设为新值;否则,什么都不做。无论如何,都返回当前值。”

std::atomic 提供了两个版本:

  • compare_exchange_weak:可能会 “虚假失败”,性能更好,通常用在循环里。

  • compare_exchange_strong:保证不虚假失败,性能稍差。

CAS 示例:实现原子乘法(原子操作不直接支持乘法,我们用 CAS 自己实现)

std::atomic<int> a{5};

void atomic_multiply(int multiplier) {
    int old_val = a.load(std::memory_order_relaxed);
    int new_val;
    
    do {
        new_val = old_val * multiplier; // 计算新值
        // 关键点:CAS
        // 如果 a 的值还是 old_val(说明期间没被别人改过),就把 a 设为 new_val
        // 如果 a 的值变了(被别人改了),old_val 会被更新为当前的新值,循环继续
    } while (!a.compare_exchange_weak(old_val, new_val, std::memory_order_relaxed));
}

int main() {
    atomic_multiply(10);
    std::cout << "a = " << a << "\n"; // 输出 50
    return 0;
}

这就是典型的 “乐观锁” 思想,假设没人竞争,先算,如果算完发现被人改了,就重试

内存序(Memory Order)

这是 C++ 并发编程中最深奥、最容易出错的部分。我们一直用的是std::memory_order_relaxed。现在我们要搞懂这到底是什么。

问题的根源:编译器和 CPU 的 “优化”

为了提高性能,编译器和CPU都会对指令进行重排序。示例:指令重排序

// 源代码
int a = 0;
int b = 0;

void thread1() {
    a = 1;  // (1)
    b = 2;  // (2)
}

在编译器或 CPU 看来,(1) 和 (2) 之间没有依赖关系,所以它们可能会被重排序成:

// 实际执行顺序(可能)
b = 2;  // (2)
a = 1;  // (1)

在单线程下,这没问题,结果都是 a=1, b=2。但在多线程下,这会导致灾难性的后果。

内存序的作用是用来禁止或限制编译器和 CPU 进行重排序的工具。它定义了一个线程对内存的修改,在什么情况下能被另一个线程看到。

C++11 提供了 6 种内存序,我们只需要掌握最核心的 3 种:

内存序

英文全称

重排序限制

性能

适用场景

memory_order_relaxed

Relaxed

几乎没有

 最快

单纯的计数器,不需要同步其他变量。

memory_order_acquire / memory_order_release

Acquire-Release

单向限制

 很快

线程间同步(生产者 - 消费者)。最常用。

memory_order_seq_cst

Sequential Consistency

完全禁止

 最慢

默认值。需要严格全局顺序。

深入详解:Acquire-Release 语义,这是实际工程中最常用、性价比最高的组合。

  • memory_order_release:用于写操作。

    • 禁止重排:在这个写操作之前的所有读写操作,都不能被重排到这个写操作之后。

  • memory_order_acquire:用于读操作。

    • 禁止重排:在这个读操作之后的所有读写操作,都不能被重排到这个读操作之前。

经典示例:生产者 - 消费者模式

#include <iostream>
#include <thread>
#include <atomic>

int data = 0; // 普通共享数据
std::atomic<bool> ready{false}; // 原子标志位

void producer() {
    data = 42; // (1) 写数据
    // 注意这里用 memory_order_release
    ready.store(true, std::memory_order_release); // (2) 发信号
}

void consumer() {
    // 注意这里用 memory_order_acquire
    while (!ready.load(std::memory_order_acquire)) { // (3) 等信号
        // 自旋等待
    }
    // 到这里,我们保证能看到 data = 42
    std::cout << "Data: " << data << "\n"; // (4) 读数据
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

发生了什么:

  1. Release 保证:(1) data = 42 一定会发生在 (2) ready.store(true) 之前。编译器不会把 (1) 重排到 (2) 后面。

  2. Acquire 保证:(4) cout << data 一定会发生在 (3) ready.load(true) 之后。编译器不会把 (4) 重排到 (3) 前面。

  3. 同步保证:一旦线程 2 在 (3) 读到了 true,它就一定能看到线程 1 在 (1) 写的 42

如果不用 Acquire-Release(用 Relaxed)会怎样?线程 2 可能会看到 ready == true,但 data 还是 0,因为指令被重排了,ready 先被设为 true,data 还没来得及写。

memory_order_seq_cst顺序一致性,这是 std::atomic 所有操作的默认内存序。它保证所有线程看到的所有原子操作的顺序都是完全一致的,就像在一个单线程里按顺序执行一样。

std::atomic<int> a{0};
// 不写内存序,默认就是 memory_order_seq_cst
a.store(10); 
// 等价于:
// a.store(10, std::memory_order_seq_cst);
  • 优点:简单,不容易出错。

  • 缺点:性能开销最大(在 x86 上开销不大,但在 ARM/PowerPC 上开销明显)

特殊原子类型:std::atomic_flag

std::atomic_flag 是最简单的原子类型,它只有两种状态:setclear。它是 C++ 标准中  唯一保证无锁 的类型,其他 std::atomic<T> 在某些平台上可能内部还是用锁实现的。

常用 API:

  • test_and_set():原子地设为 true,并返回之前的值。

  • clear():原子地设为 false

用 std::atomic_flag 实现一个自旋锁:

#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

class SpinLock {
private:
    // ATOMIC_FLAG_INIT 初始化为 clear(false)状态
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        // test_and_set:如果之前是 clear,就设为 set 并返回 false(成功获取锁)
        // 如果之前是 set,就返回 true(循环等待,自旋)
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 这里什么都不做,就是循环等
            // 这就是“自旋”
        }
    }
    
    void unlock() {
        // 释放锁,设为 clear
        flag.clear(std::memory_order_release);
    }
};

SpinLock spinlock;
int counter = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        spinlock.lock();
        counter++;
        spinlock.unlock();
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }
    for (auto& t : threads) t.join();
    
    std::cout << "Counter: " << counter << "\n"; // 正确输出 100000
    return 0;
}

自旋锁的特点是:如果锁被占用,线程不睡眠,而是原地空转(自旋)等待,直到锁可用。

自旋锁 vs 互斥锁:

  • 自旋锁:等待时不睡眠,占用 CPU 但响应快。适用于锁持有时间非常短(比如只是加个计数器)的场景。

  • 互斥锁:等待时睡眠,不占用 CPU 但响应慢。适用于锁持有时间长的场景。

  • 核心操作:load(), store(), fetch_add(), compare_exchange_weak()

  • 内存序:

    • relaxed:最快,只保证原子性,不管顺序(计数器)。

    • acquire/release:最常用,保证线程间同步(生产者 - 消费者)。

    • seq_cst:默认,最强保证,最慢。

建议

1. 不要对 std::atomic 变量使用普通的 ++ 或 +=虽然 std::atomic<int> 重载了 operator++,但推荐使用显式的 fetch_add(1),因为这样可以显式指定内存序,代码更清晰。

std::atomic<int> a{0};
a++; // 可以,但默认用 memory_order_seq_cst,可能慢
a.fetch_add(1, std::memory_order_relaxed); // 推荐,显式控制

2. 不是所有类型都能无锁

std::atomic<T> 有一个成员函数 is_lock_free()

  • 对于 int, long, void* 等基本类型,通常是 true(无锁)。

  • 对于 std::string 或自定义的大结构体,通常是 false(内部还是用锁实现的,性能还不如直接用 std::mutex)。

3. 优先使用 memory_order_relaxed 或 acquire/release,除非你真的需要严格的顺序一致性,否则不要用默认的 memory_order_seq_cst,它会带来不必要的性能损失。

4. 自旋锁:用 std::atomic_flag 实现,适用于短时间持有锁的场景。

QA:

为什么用 Atomic?

Mutex 太慢(上下文切换、内核态),Atomic 利用 CPU 硬件指令实现无锁并发,性能高。

memory_order 有哪些?最常用哪个?

主要有三个级别:

  1. relaxed:最快,只保证原子性,不保证内存顺序;

  2. acquire/release:最常用,保证 “Acquire 之后的读写一定能看到 Release 之前的读写”;

  3. seq_cst:最强,最慢,保证顺序一致性(默认值)。最常用 acquire/release,平衡性能和同步需求。


noexcept:并发性能优化的隐形推手

设计原因

并发场景下,移动操作经常发生在 vector reallocate、thread 构造等地方。noexcept 可以让编译器和标准库进行更激进的优化。

底层原理

std::vector 扩容、std::thread 内部等并发相关容器操作中,只有 noexcept 的移动构造函数才会被优先使用。

实际例子:

class Task {
public:
    Task(Task&& other) noexcept { ... }   // 必须 noexcept
};
std::vector<Task> tasks;
tasks.push_back(Task{});   // 会使用 noexcept 移动

在并发代码中为什么要给移动构造函数加 noexcept

std::vector 等容器在 reallocate 时,只有 noexcept 的移动才会使用,否则退化为拷贝,性能大幅下降,同时也影响线程池、任务队列等并发数据结构的效率。

现代并发代码中,几乎所有自定义可移动类型都会把移动构造函数标记为 noexcept

thread_localnoexcept 是 C++11 为并发编程准备的两把利器 , 前者从数据隔离层面解决了线程安全的痛点,后者从性能优化层面为并发代码提速。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐