C++标准线程库-全面讲解

自 C++11 标准引入以来,C++ 终于拥有了跨平台的原生多线程支持,不再需要依赖操作系统的 API(如 Linux 的 pthreads 或 Windows 的 CreateThread)。

本指南涵盖从基础到 C++20 的高级特性,分为八大模块。

  1. 基础架构与头文件

  2. 线程管理 (std::thread)

  3. 互斥与死锁防护 (std::mutex 家族)

  4. 线程同步与通信 (std::condition_variable)

  5. 异步编程与返回值 (std::future)

  6. 原子操作与无锁编程 (std::atomic)

  7. C++20 新特性 (std::jthread, 信号量, 屏障)

  8. 最佳实践与常见陷阱

基础架构与头文件

C++ 并发编程的功能分散在以下几个标准头文件中:

|---------------------------------|-------------|---------------------------------------------------------------|
| 头文件 | 核心功能 | 关键类/关键字 |
| <thread> | 线程创建与管理 | std::thread, std::this_thread |
| <mutex> | 互斥量(锁) | std::mutex, std::lock_guard, std::unique_lock, std::call_once |
| <condition_variable> | 条件变量(等待/通知) | std::condition_variable |
| <future> | 异步任务与结果获取 | std::future, std::promise, std::async |
| <atomic> | 原子操作(无锁) | std::atomic |
| <semaphore> (C++20) | 信号量 | std::counting_semaphore |
| <latch> / <barrier> (C++20) | 线程屏障 | std::latch, std::barrier |

一、 线程主体 <thread>

1. std::thread 类

构造函数 (Constructors)

cpp 复制代码
#include <thread>
template< class Function, class... Args >
explicit thread( Function&& func, Args&&... args );

作用:创建新线程,指定执行函数及参数(对应 pthread_create)。

特性

  1. 构造函数:std::thread t(func, args...)func 为线程执行函数,args 为传递给函数的参数;
  2. func几乎可以接受任何形式的函数, func可以带返回值但是会被忽略
  3. 强制规则:线程对象创建后必须调用 join()detach() ,否则析构时会调用 std::terminate() 终止整个程序;
  4. 可移动不可拷贝:std::thread 是移动类型(std::move 转移所有权),不能直接赋值 / 拷贝。

基础示例

cpp 复制代码
#include <iostream>
#include <thread>

// 1. 普通函数
void func(int x) { std::cout << "Func: " << x << "\n"; }

// 2. 仿函数 (Functor)
struct Task {
    void operator()() { std::cout << "Functor\n"; }
};

int main() {
    // 启动方式 1: 函数指针
    std::thread t1(func, 10);

    // 启动方式 2: Lambda (最常用)
    std::thread t2([](int x) { 
        std::cout << "Lambda: " << x << "\n"; 
    }, 20);

    // 启动方式 3: 仿函数对象
    std::thread t3(Task());

    // 启动方式 4: 类的成员函数 (必须传递对象指针)
    struct Worker {
        void work(int n) { std::cout << "Member: " << n << "\n"; }
    };
    Worker w;
    std::thread t4(&Worker::work, &w, 30); // &w 充当 this 指针

    t1.join(); t2.join(); t3.join(); t4.join();
}

1. 普通函数 (无参数)

cpp 复制代码
void hello() { /*...*/ }

// Function = void(*)()
// Args 为空
std::thread t(hello);

2. 带参数的函数 (值传递)

cpp 复制代码
void add(int a, int b) { /*...*/ }

// Function = void(*)(int, int)
// Args = int, int
std::thread t(add, 10, 20);

3. 引用传递 (必须使用 std::ref)

这是新手最容易踩的坑。 因为 std::thread 内部默认会拷贝参数,如果你想传引用,必须用 std::ref 包装。

cpp 复制代码
void update(int& n) { n++; }

int x = 0;
// 错误!编译报错或无法修改 x,因为 thread 内部拷贝了一份 x
// std::thread t1(update, x); 

// 正确:使用 std::ref 告诉 thread 这是一个引用
std::thread t2(update, std::ref(x));

4. 类成员函数 (绑定 this 指针)

由于成员函数隐含一个 this 指针参数,所以必须把对象的地址传进去。

cpp 复制代码
class Worker {
public:
    void doWork(int id) { /*...*/ }
};

Worker w;
// Function = void(Worker::*)(int)
// Args = Worker*, int
// 参数1:成员函数地址
// 参数2:对象指针 (即 this)
// 参数3:函数的参数
std::thread t(&Worker::doWork, &w, 99);

5. 独占指针 (Move Only 类型)

如果参数是 std::unique_ptr 这种不能拷贝只能移动的类型,std::thread 会自动处理移动语义。

cpp 复制代码
void process(std::unique_ptr<int> ptr) { /*...*/ }

auto p = std::make_unique<int>(100);
// p 被移动进线程,主线程中 p 变为空
std::thread t(process, std::move(p));

成员函数 (Member Functions)

  • join(): 阻塞。当前线程停下来,等待该 thread 对象代表的线程执行完毕。
  • detach(): 分离。切断对象与内核线程的联系。内核线程在后台运行,对象不再控制它。
  • joinable(): 检查。返回 bool。如果线程还在运行且没被 join/detach,返回 true。析构前必须确保此函数返回 false。
  • get_id(): 获取该线程的唯一 ID (std::thread::id)。
  • native_handle(): 返回底层句柄(如 Linux 的 pthread_t 或 Windows 的 HANDLE),用于调用系统原生 API。
  • swap(thread& other): 交换两个线程对象的底层句柄。

等待线程终止:join()

作用:阻塞调用线程(如主线程),直到目标线程执行完毕,回收线程资源(对应 pthread_join);虽然线程函数返回清理了栈内存,但线程对象本身(OS 内核层面的线程描述符)需要通过 join() 来确认并回收。如果不 join,std::thread 析构时会崩溃。

cpp 复制代码
void std::thread::join();

关键特性

  1. 阻塞(Block):调用 t.join() 的线程(通常是主线程)会被卡住,直到线程 t 执行完毕(正常返回)。
  2. 异常安全:若主线程抛出异常,可能导致 join() 未调用,建议用 RAII 封装(如 std::scoped_thread,C++20);
  3. 状态变化:调用后线程对象变为 "非可连接状态",joinable() 返回 false

示例(异常安全的 join)

cpp 复制代码
#include <iostream>
#include <thread>
#include <stdexcept>
using namespace std;

void risky_task() {
    throw runtime_error("子线程抛出异常");
}

// RAII 封装:自动 join 线程
class ThreadGuard {
public:
    explicit ThreadGuard(thread& t) : t_(t) {}
    ~ThreadGuard() {
        if (t_.joinable()) { // 检查是否可 join
            t_.join();
        }
    }
    // 禁止拷贝(避免多次 join)
    ThreadGuard(const ThreadGuard&) = delete;
    ThreadGuard& operator=(const ThreadGuard&) = delete;
private:
    thread& t_;
};

int main() {
    thread t(risky_task);
    ThreadGuard guard(t); // 析构时自动 join
    return 0;
}

设置分离状态:detach()

作用:将线程设置为 "分离状态",线程结束后由系统自动回收资源(对应 pthread_detach);

cpp 复制代码
void std::thread::detach();

作用与机制

  • 分离(Sever Connection):切断 std::thread 对象与底层 OS 线程的联系。

  • 守护线程(Daemon):该线程会在后台继续运行,直到函数结束或者进程退出时被强制终止。

  • 自动回收:当该后台线程运行结束时,C++ 运行时库和操作系统会自动回收其资源,不需要主线程操心。

  • 资源安全 :分离线程禁止访问主线程的局部变量(生命周期可能先结束);

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void background_task() {
    for (int i = 0; i < 5; i++) {
        this_thread::sleep_for(chrono::seconds(1));
        cout << "分离线程运行中..." << endl;
    }
}

int main() {
    thread t(background_task);
    t.detach(); // 设置为分离状态
    cout << "主线程继续执行,无需等待分离线程" << endl;
    // 主线程休眠 6 秒,确保分离线程执行完毕
    this_thread::sleep_for(chrono::seconds(6));
    return 0;
}

风险(为什么推荐度低)

  • 生命周期失控:如果分离的线程引用了主线程的局部变量(例如 int&),而主线程先结束了,该变量被销毁,子线程就会访问非法内存(悬空引用),导致难以调试的崩溃。

  • 无法通过常规手段控制停止:一旦分离,你就失去了它的句柄(Handle),无法再 join 它或控制它。

强制终止: std::terminate()

这是"核选项",通常不是用来终止线程的,而是用来杀死整个进程的。

cpp 复制代码
[[noreturn]] void std::terminate() noexcept;

作用与机制

  • 立即崩溃:它会调用当前的 terminate_handler,默认情况下是调用 std::abort()。这会导致整个程序(Process)立即退出,不仅是当前线程。

  • 不清理资源 :此时栈上的对象不会调用析构函数,打开的文件可能没关闭,缓冲区数据可能没写入磁盘。

触发场景(通常是被动触发)

  • std::thread 对象析构时,如果线程还是 joinable 状态(既没 join 也没 detach)。

  • 线程抛出了异常,但这异常没有被 try-catch 捕获。

为什么禁止使用:除非遇到无法修复的致命错误需要立即把程序杀掉,否则绝不应该主动调用它来结束线程。

获取当前线程的ID: get_id()

作用:获取当前线程的唯一标识符(ID)。

返回值:std::thread::id 类型(通常是一个数字,但标准库封装成了对象)。

比喻:就像每个人都有身份证号。在多线程调试时,你要知道"这句话是哪个线程打印出来的",就需要看 ID。

应用场景

  • 日志打印:调试多线程程序时区分日志来源。

  • 主线程判断:检查当前是否在主线程中运行。

  • 数据关联:用 ID 作为 std::map 的 Key,为不同线程存储私有数据。

cpp 复制代码
#include <iostream>
#include <thread>

void worker() {
    // 获取当前(子)线程的 ID
    std::cout << "子线程 ID: " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t(worker);
    // 获取主线程的 ID
    std::cout << "主线程 ID: " << std::this_thread::get_id() << std::endl;
    t.join();
    return 0;
}

静态成员函数 (Static Functions)

  • std::thread::hardware_concurrency(): 返回 CPU 核心数(逻辑核心)。如果无法获取,返回 0。常用于决定线程池的大小。

2. std::this_thread 命名空间

这里的函数只能控制当前正在运行代码的那个线程

  • get_id(): 获取当前线程 ID。

  • yield(): 主动让出 CPU 时间片,重新进入调度队列。

  • sleep_for(duration): 睡眠指定时长(如 2s)。

  • sleep_until(time_point): 睡眠直到某个绝对时间点(如明天 8:00)。

1. get_id():我是谁?

cpp 复制代码
std::thread::id get_id() noexcept;

作用:获取当前线程的唯一标识符(ID)。

返回值:std::thread::id 类型(通常是一个数字,但标准库封装成了对象)。

生活比喻:就像每个人都有身份证号。在多线程大合唱时,你要知道"刚才这句歌词是哪个人唱的",就需要看 ID。

应用场景

  • 调试日志 :在打印 Log 时加上 ID,一眼看出日志是哪个线程打印的,排查死锁或乱序执行的神器。

  • 逻辑判断 :比如主线程 ID 是 1001,如果检测到当前 ID 不是 1001,就不执行某些 UI 更新操作。

  • 数据关联 :用 ID 作为 std::map 的 Key,为不同线程存储私有数据。

    #include <iostream>
    #include <thread>

    void worker() {
    // 打印:我是子线程,我的工号是 xxx
    std::cout << "子线程 ID: " << std::this_thread::get_id() << std::endl;
    }

    int main() {
    std::thread t(worker);
    // 打印:我是主线程,我的工号是 yyy
    std::cout << "主线程 ID: " << std::this_thread::get_id() << std::endl;
    t.join();
    return 0;
    }

在 C++ 标准线程库中,有两个长得一模一样的 get_id(),但它们的所属对象使用场景 完全不同。

  1. 一个是问 "我自己是谁?" (std::this_thread)

  2. 一个是问 "那个线程对象是谁?" (std::thread 成员函数)


2. yield():让出自己的时间片

cpp 复制代码
void yield() noexcept;

作用放弃当前的 CPU 时间片,把 CPU 让给其他线程使用。

底层机制:调用该函数的线程会告诉操作系统调度器:"我现在的任务不急,或者我在等某个锁,你可以先把 CPU 分给其他正在排队(Ready 状态)的线程用一下。"

关键点

  • 线程不会进入睡眠(阻塞)状态。它只是从"运行中(Running)"变回"排队中(Ready)"。

  • 如果此时没有其他线程在排队,操作系统可能立刻又让它继续运行。

  • 生活比喻"你先走, 我重新排队"(只要轮到我,我马上回来)"

应用场景

  • 自旋锁(Spinlock) :当线程在 while 循环里等待某个条件变真时,如果一直空转(死循环)会把 CPU 跑到 100%。加上 yield() 可以降低 CPU 占用,避免"占着茅坑不拉屎",饿死其他低优先级线程。
cpp 复制代码
// 典型的忙等待(Busy Wait)优化
while (!data_is_ready) {
    // 不要死循环空跑,太费电且独占CPU
    std::this_thread::yield(); // 稍微让一下,等下一轮调度再来看 ready 变没变
}

3. sleep_for():我要睡一会儿(相对时间)

cpp 复制代码
template< class Rep, class Period >
void sleep_for( const std::chrono::duration<Rep, Period>& rel_time );

作用 :让当前线程暂停执行指定的一段时间

参数:std::chrono::duration(时间段,例如:2秒、500毫秒)。

底层机制 :线程进入阻塞(Blocked)状态,完全释放 CPU 资源。操作系统承诺在时间没到之前,绝对不会唤醒你(除非被信号打断)。

生活比喻:设一个倒计时闹钟,"我要午休 10 分钟"。

应用场景

  • 模拟耗时:测试时模拟网络延迟。

  • 降低频率:比如一个后台线程每隔 1 秒检查一次服务器状态,不需要一直跑。

cpp 复制代码
#include <thread>
#include <chrono>
using namespace std::chrono_literals; // 允许使用 2s, 100ms 这种写法

void worker() {
    // 线程暂停 2 秒,期间 CPU 占用率为 0
    std::this_thread::sleep_for(2s); 
    // 或者 std::this_thread::sleep_for(std::chrono::milliseconds(500));
}

4. sleep_until():我要睡到几点(绝对时间)

cpp 复制代码
template< class Clock, class Duration >
void sleep_until( const std::chrono::time_point<Clock, Duration>& abs_time );

作用 :让当前线程暂停执行,直到某个具体的时间点

参数:std::chrono::time_point(时间点,例如:明天早上 8:00)。

区别:sleep_for 是"睡多久",sleep_until 是"睡到什么时候"。

生活比喻:设一个定点闹钟,"把闹钟定在 12:00:00 响"。

应用场景

消除时间漂移(Drift):如果你写一个节拍器,要求严格每隔 1 秒响一次。

用 sleep_for(1s):因为代码执行本身要几毫秒,循环几次后,时间会慢慢变慢(1.01s, 2.02s, 3.03s...)。

用 sleep_until(next_time):每次循环都计算下一个整点时间,保证每次都在整点醒来,误差不累积。

cpp 复制代码
#include <thread>
#include <chrono>

void worker() {
    auto now = std::chrono::steady_clock::now();
    // 计算出未来 5 秒后的那个时间点
    auto wake_up_time = now + std::chrono::seconds(5);
    
    // 睡到那个时间点再醒来,不管中间发生了什么
    std::this_thread::sleep_until(wake_up_time);
}

总结对比

|-----------------|---------------------|--------------|---------------|--------------|
| 函数 | 状态变化 | CPU 占用 | 核心含义 | 典型场景 |
| get_id | 无 | 正常 | 我是谁? | 调试日志、数据映射 |
| yield | Running -> Ready | (但不为0) | 我不急,你们先用 | 自旋锁、避免死循环空转 |
| sleep_for | Running -> Blocked | 0 (完全释放) | 我要休息 X 分钟 | 定时器、轮询间隔 |
| sleep_until | Running -> Blocked | 0 (完全释放) | 我要睡到 X 点钟 | 精确周期的任务、定时启动 |

关键提示

不要用 yield 来代替 sleep 做延时。yield 只是重新排队,如果当时只有你一个人在排队,操作系统会立刻把你叫回来继续干活,起不到"等待"的效果,CPU 依然会很高。想要等待,请用 sleep。

二、 互斥与锁 <mutex> & <shared_mutex>

1. 互斥量 (Mutex Types)

std::mutex(互斥量)是 C++ 多线程编程中保护共享数据、避免**数据竞争(Data Race)**的最基本工具。它的核心思想是:"同一时刻,只允许一个线程访问临界区"。

加锁会导致效率降低, 一定要把控好加锁的力度

这些是底层的锁对象,不建议直接调用其 member functions,建议配合 RAII 包装器使用

|--------------------------------|---------------------------|-----------------------------------------------------------------------------------|
| 类型 | 特性 | 成员函数 |
| std::mutex | 最基本、最高效、不可递归 | lock(), unlock(), try_lock() |
| std::recursive_mutex | 同一线程可多次 lock,需相同次数 unlock | 同上 |
| std::timed_mutex | 支持超时等待 | 同上 + try_lock_for(), try_lock_until() |
| std::recursive_timed_mutex | 可递归 + 超时 | 同上 |
| std::shared_mutex (C++17) | 读写锁 (多读单写) | lock(), unlock() (写锁)<br>lock_shared(), unlock_shared() (读锁)<br>以及对应的 try_... |

std::mutex (最基本互斥锁)

特点:独占、不可递归。最常用,性能最高。

核心签名 (Member Functions)

头文件:<mutex>

cpp 复制代码
class mutex {
public:
    // 构造函数
    constexpr mutex() noexcept;
    // 禁止拷贝和移动
    mutex(const mutex&) = delete;

    // 1. 阻塞等待锁。如果锁被别人拿了,就一直等(死等)。
    void lock(); 

    // 2. 尝试拿锁。如果拿到了返回 true;如果锁被别人拿了,立刻返回 false,不等待。
    bool try_lock(); 

    // 3. 释放锁。
    void unlock(); 
};

实战实例

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // 全局互斥量
int g_num = 0;

void slow_increment(int id) {
    // 尝试拿锁,拿不到就去干别的
    if (mtx.try_lock()) {
        ++g_num;
        std::cout << "线程 " << id << " 抢到了锁,g_num=" << g_num << "\n";
        mtx.unlock(); // 记得手动解锁!
    } else {
        std::cout << "线程 " << id << " 没抢到锁,溜了\n";
    }
}

// ⚠️ 最佳实践:实际开发中尽量不要手动调用 lock/unlock,
// 而是用 std::lock_guard<std::mutex> guard(mtx);

std::recursive_mutex (递归互斥锁)

特点 :允许同一个线程多次获取同一把锁,而不会死锁。主要用于递归函数。

核心签名

头文件:<mutex>

接口与 std::mutex 完全一致,但在逻辑上允许重入。

cpp 复制代码
void lock();      // 计数器 +1
bool try_lock();  // 计数器 +1
void unlock();    // 计数器 -1,当减到 0 时才真正释放给其他线程

实战实例

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::recursive_mutex r_mtx;

void recursive_function(int n) {
    if (n <= 0) return;

    r_mtx.lock(); // 第 n 次加锁
    std::cout << "Level " << n << " locked\n";
    
    recursive_function(n - 1); // 递归调用,再次加锁
    
    std::cout << "Level " << n << " unlocked\n";
    r_mtx.unlock(); // 第 n 次解锁
}

int main() {
    std::thread t(recursive_function, 3);
    t.join();
}
// 如果这里用普通的 std::mutex,程序会直接卡死(死锁)。

std::timed_mutex (超时互斥锁)

特点:拿锁的时候可以设置"耐心值",等一会等不到就放弃。

核心签名

头文件:<mutex>

除了基础的 lock, unlock, try_lock 外,增加了:

cpp 复制代码
// 1. 尝试拿锁,最多等 duration 时间
template <class Rep, class Period>
bool try_lock_for(const std::chrono::duration<Rep, Period>& rel_time);

// 2. 尝试拿锁,直到某个绝对时间点
template <class Clock, class Duration>
bool try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time);

实战实例

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex t_mtx;

void worker(int id) {
    using namespace std::chrono_literals;
    
    // 尝试拿锁,最多等 200 毫秒
    if (t_mtx.try_lock_for(200ms)) {
        std::cout << "线程 " << id << " 拿到锁了!\n";
        std::this_thread::sleep_for(1s); // 模拟干活 1 秒
        t_mtx.unlock();
    } else {
        std::cout << "线程 " << id << " 等了 200ms 没等到,放弃。\n";
    }
}

int main() {
    std::thread t1(worker, 1); // t1 先拿锁,占 1 秒
    std::this_thread::sleep_for(10ms); // 确保 t1 先运行
    std::thread t2(worker, 2); // t2 只能等 200ms,肯定等不到

    t1.join();
    t2.join();
}

std::recursive_timed_mutex (递归超时锁)

特点:就是 recursive 和 timed 的缝合怪。既可以递归重入,又可以设置超时。

核心签名

头文件:<mutex>

集合了上述所有的功能:

  • lock(), unlock(), try_lock()

  • try_lock_for(), try_lock_until()

(实例略,逻辑同上,只是允许在递归函数里用 try_lock_for)。

std::shared_mutex (读写锁 / 共享互斥锁)

特点C++17 引入。区分"读者"和"写者"。

  • 读锁 (Shared):允许多个线程同时读。

  • 写锁 (Exclusive):只允许一个线程写,写的时候谁都不能读。

核心签名

头文件:<shared_mutex>

cpp 复制代码
class shared_mutex {
public:
    // === 写锁接口 (排他锁) ===
    void lock();      // 拿写锁,堵塞所有读和写
    bool try_lock();
    void unlock();

    // === 读锁接口 (共享锁) ===
    void lock_shared();      // 拿读锁。如果有写锁在,就等;如果是读锁在,直接进。
    bool try_lock_shared();
    void unlock_shared();
};

实战实例

通常配合 std::unique_lock (写) 和 std::shared_lock (读) 使用。

cpp 复制代码
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <mutex>
#include <vector>

std::shared_mutex rw_mtx; // 读写锁
int data_store = 0;

// 写者:修改数据 (排他)
void writer() {
    // 获取写锁 (unique_lock 会调用 mtx.lock())
    std::unique_lock<std::shared_mutex> lock(rw_mtx);
    data_store++;
    std::cout << "Writer 修改数据为: " << data_store << "\n";
}

// 读者:读取数据 (共享)
void reader(int id) {
    // 获取读锁 (shared_lock 会调用 mtx.lock_shared())
    std::shared_lock<std::shared_mutex> lock(rw_mtx);
    std::cout << "Reader " << id << " 读取数据: " << data_store << "\n";
    // 多个 Reader 可以同时打印这句话,不用排队
}

int main() {
    std::thread t1(reader, 1);
    std::thread t2(reader, 2);
    std::thread t3(writer);
    std::thread t4(reader, 3);

    t1.join(); t2.join(); t3.join(); t4.join();
}

总结对照表

|--------------------------|-----|-----|-------|--------------------|
| 锁类型 | 递归? | 超时? | 读写分离? | 核心用途 |
| std::mutex | ❌ | ❌ | ❌ | 90% 的场景。保护普通数据。 |
| std::recursive_mutex | ✅ | ❌ | ❌ | 递归函数、复杂的嵌套调用。 |
| std::timed_mutex | ❌ | ✅ | ❌ | 避免长时间死等,防止 GUI 卡死。 |
| std::shared_mutex | ❌ | ❌ | ✅ | 读多写少(如配置中心、缓存)。 |

2. RAII 锁包装器 (Lock Wrappers)

这些是管理 Mutex 生命周期的类,构造时加锁,析构时解锁

std::lock_guard<Mutex>

  • 最轻量

  • 构造函数:lock_guard(m) (立即锁), lock_guard(m, std::adopt_lock) (接管已锁的互斥量)。

  • 无其他成员函数(不能手动 unlock)。

std::unique_lock<Mutex>

  • 最灵活(配合条件变量必须用它)。

  • 构造函数:支持 std::defer_lock (先不锁), std::try_to_lock (尝试锁), std::adopt_lock。

  • 成员函数:lock(), unlock(), try_lock(), release() (释放所有权), owns_lock() (检查是否持有锁)。

std::scoped_lock<M...> (C++17)

  • 防死锁。可以同时接收多个互斥量:scoped_lock g(m1, m2, m3);。内部使用死锁避免算法。

std::shared_lock<SharedMutex> (C++14)

  • 专门用于获取 shared_mutex 的读锁(共享锁)。用法同 unique_lock。

std::lock_guard (最轻量)

核心特点"死板"。一旦创建必须持有锁,直到析构才释放。不能手动解锁,没有额外内存开销。

核心签名

cpp 复制代码
template <class Mutex>
class lock_guard {
public:
    // 1. 基础构造:立即调用 m.lock()
    explicit lock_guard(mutex_type& m);

    // 2. 领养构造:假设 m 已经被当前线程锁住了,这里只负责接管析构解锁权
    // (std::adopt_lock 是一个标记结构体)
    lock_guard(mutex_type& m, std::adopt_lock_t t);

    // 析构:调用 m.unlock()
    ~lock_guard();
    
    // 禁用拷贝和移动
    lock_guard(const lock_guard&) = delete;
};

实战实例

cpp 复制代码
#include <mutex>
#include <iostream>

std::mutex mtx;
int g_i = 0;

void safe_increment() {
    // 【常规用法】
    // 构造时立刻锁住 mtx,退出函数作用域时自动解锁
    std::lock_guard<std::mutex> lock(mtx);
    g_i++;
    
    // lock.unlock(); // ❌ 错误!lock_guard 没有 unlock 成员函数
} 

void adopt_example() {
    mtx.lock(); // 手动上锁 (比如是在别的函数里锁的)
    
    // 【领养用法】
    // 告诉 lock_guard:"我已经锁好了,你别再锁了,但你要负责帮我解锁"
    std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
    
    // ... 操作 ...
} // 自动 unlock

std::unique_lock (最灵活)

核心特点"自由"。持有锁的所有权,但可以随时抛弃、延迟获取或移交。比 lock_guard 稍微慢一点点(内部有个 bool 标志记录是否持有锁)。

核心签名

cpp 复制代码
template <class Mutex>
class unique_lock {
public:
    // 1. 基础构造:立即 lock()
    explicit unique_lock(mutex_type& m);

    // 2. 延迟锁定:构造时不锁,留着以后手动调 lock()
    unique_lock(mutex_type& m, std::defer_lock_t t) noexcept;

    // 3. 尝试锁定:构造时调 try_lock(),不阻塞
    unique_lock(mutex_type& m, std::try_to_lock_t t);

    // 4. 领养锁定:同 lock_guard
    unique_lock(mutex_type& m, std::adopt_lock_t t);

    // 成员函数
    void lock();
    void unlock();
    bool try_lock();
    bool owns_lock() const; // 检查当前是否锁着
    Mutex* release();       // 放弃管理,返回原始互斥量指针(且不解锁)
};

实战实例

cpp 复制代码
#include <mutex>
#include <iostream>

std::mutex mtx;

void flexible_worker() {
    // 【延迟锁定策略】
    // 此时 mtx 还没有被锁住!
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);

    // ... 做一些不需要锁的准备工作 ...
    
    // 现在需要锁了,手动锁
    lock.lock(); 
    std::cout << "Critical section" << std::endl;
    
    // 可以手动解锁,去处理别的事
    lock.unlock(); 
    
    // ... 做一些耗时操作 ...
    
    // 再次加锁
    lock.lock(); 
    
} // 析构时:如果当前拿着锁,就 unlock;如果没拿,啥也不干

std::scoped_lock (C++17 防死锁神器)

核心特点"通吃"。它是 lock_guard 的升级版,支持同时锁多个互斥量。它内部使用了死锁避免算法(类似于 std::lock()),保证以安全的顺序加锁。

核心签名

cpp 复制代码
// 变长模板参数,支持 1 个或多个互斥量
template <class... MutexTypes>
class scoped_lock {
public:
    // 构造函数:原子的锁定所有 m...
    explicit scoped_lock(MutexTypes&... m);
    
    ~scoped_lock();
    
    // 同样禁用拷贝
};

实战实例

经典场景:银行转账(需要同时锁住账户 A 和账户 B,否则容易死锁)。

cpp 复制代码
#include <mutex>

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

void transfer(Account& from, Account& to, int amount) {
    // 【C++17 之前】如果你先锁 from 再锁 to,而在另一个线程先锁 to 再锁 from,就会死锁。
    
    // 【C++17 之后】scoped_lock 会自动处理加锁顺序,保证不死锁
    // 无论参数顺序是 (from, to) 还是 (to, from),它都很安全
    std::scoped_lock lock(from.m, to.m);
    
    from.balance -= amount;
    to.balance += amount;
} // 两个锁同时释放

std::shared_lock (C++14 读写锁专用)

核心特点"只读模式"。它是 shared_mutex 的最佳拍档。它的用法和 unique_lock 几乎一模一样,区别在于它调用的是底层的 lock_shared() 而不是 lock()。

核心签名 (Class Interface)

cpp 复制代码
template <class Mutex>
class shared_lock {
public:
    // 构造时调用 m.lock_shared() (获取读锁)
    explicit shared_lock(mutex_type& m);
    
    // 支持 defer_lock, try_to_lock 等策略
    shared_lock(mutex_type& m, std::defer_lock_t t) noexcept;

    // 析构调用 m.unlock_shared()
    ~shared_lock();
    
    void lock(); // 实际调用 lock_shared()
    void unlock(); // 实际调用 unlock_shared()
};

实战实例

cpp 复制代码
#include <shared_mutex>
#include <mutex>
#include <iostream>

std::shared_mutex rw_mtx; // 读写锁
int data = 0;

// 读者线程(可以多个同时进)
void reader() {
    // 使用 shared_lock 获取"共享锁/读锁"
    std::shared_lock<std::shared_mutex> lock(rw_mtx);
    std::cout << "Reading: " << data << std::endl;
} // 自动调用 unlock_shared

// 写者线程(排他,只能进一个)
void writer() {
    // 写者必须用 unique_lock (因为它需要独占锁 lock())
    std::unique_lock<std::shared_mutex> lock(rw_mtx);
    data++;
    std::cout << "Writing: " << data << std::endl;
} // 自动调用 unlock

总结选择指南

  1. 绝大多数情况 (只锁一个,锁整个作用域):用 std::lock_guard (如果你用 C++17,推荐直接用 std::scoped_lock 代替 lock_guard,效果一样且更通用)。

  2. 需要手动解锁、延迟加锁、或配合条件变量 :必须用 std::unique_lock

  3. 同时锁两个及以上互斥量 :必须用 std::scoped_lock

  4. 读取 shared_mutex :必须用 std::shared_lock

3. 通用锁定函数

这三个函数是 C++ <mutex> 头文件中用于复杂锁管理和初始化的核心工具函数。它们都是函数模板,支持可变参数。

  • std::lock(m1, m2, ...);

    锁定多个互斥量,使用死锁避免算法(但不负责解锁,需配合 adopt_lock 使用)。

  • std::try_lock(m1, m2, ...);

    尝试锁定多个,按顺序锁,失败则回滚。

  • std::call_once(flag, func, args...);

    配合 std::once_flag 使用,确保函数在多线程环境下只执行一次(单例模式神器)。

std::lock (死锁避免算法)

功能 :同时锁定两个或更多的互斥量。它使用特殊的算法(通常是"尝试-回退"策略)来保证不会发生死锁
注意 :它只负责"上锁",不负责"解锁"。因此,锁完之后,必须立刻将锁的所有权交给 RAII 包装器(如 lock_guard 或 unique_lock)来管理析构解锁,这就需要用到 std::adopt_lock 策略。

核心签名

cpp 复制代码
// 位于 <mutex>
template< class L1, class L2, class... L3 >
void lock( L1& m1, L2& m2, L3&... m3 );
  • 参数:接收 2 个或多个互斥量(引用)。

  • 异常:如果抛出异常,所有已获取的锁都会被自动释放。

实战实例

经典场景:银行转账(需要同时锁住两个账户,防止 A 转 B 的同时 B 转 A 造成死锁)。

cpp 复制代码
#include <mutex>
#include <thread>
#include <iostream>

struct BankAccount {
    int balance;
    std::mutex m;
};

void transfer(BankAccount& from, BankAccount& to, int amount) {
    // 1. 使用 std::lock 同时锁住两个锁,避免死锁
    // 如果这里直接 from.m.lock(); to.m.lock(); 就可能死锁
    std::lock(from.m, to.m);

    // 2. 关键步骤:领养锁 (Adopt Lock)
    // 告诉 lock_guard:"锁我已经上好了,你别再上锁了,但你要负责帮我解锁"
    std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);

    // 3. 安全操作
    from.balance -= amount;
    to.balance += amount;
}

std::try_lock (顺序尝试与回滚)

功能:尝试依次锁定传入的互斥量 m1, m2, ...。

  • 成功:如果所有锁都抢到了,返回 -1。

  • 失败 :只要遇到任何一个锁抢失败(被占用了),它会立刻释放之前已经抢到的所有锁(回滚) ,并返回导致失败的那个锁的索引(从 0 开始)。

核心签名

cpp 复制代码
// 位于 <mutex>
template< class L1, class L2, class... L3 >
int try_lock( L1& m1, L2& m2, L3&... m3 );
  • 返回值 :int。-1 表示成功,0 表示第 1 个失败,1 表示第 2 个失败,以此类推。

实战实例

场景:我想吃顿饭,需要同时拿到"碗"和"筷子"。如果拿到了碗但没拿到筷子,我就把碗放回去,别占着资源不干活。

cpp 复制代码
#include <mutex>
#include <iostream>

std::mutex bowl_mtx;
std::mutex chopstick_mtx;

void eat_dinner() {
    // 尝试同时拿碗和筷子
    int result = std::try_lock(bowl_mtx, chopstick_mtx);

    if (result == -1) {
        // === 成功拿到所有锁 ===
        std::cout << "拿到了碗和筷子,开吃!\n";
        
        // 同样需要领养,防止忘记解锁
        std::lock_guard<std::mutex> lg1(bowl_mtx, std::adopt_lock);
        std::lock_guard<std::mutex> lg2(chopstick_mtx, std::adopt_lock);
    } 
    else {
        // === 失败 ===
        // 此时 std::try_lock 已经自动帮你回滚了解锁,你不需要手动 unlock
        std::cout << "获取资源失败,失败的锁索引是: " << result << "\n";
    }
}

std::call_once (线程安全的单例/初始化)

功能 :保证某个函数在多线程环境下只被执行一次 。通常用于懒加载(Lazy Initialization)。
机制:它依赖一个 std::once_flag 标志位。

  • 如果有多个线程同时调用 call_once,只有一个会成功执行函数。

  • 其他线程会阻塞等待,直到执行者成功完成。

  • 如果执行者抛出异常,标志位不会翻转,其他线程会再次尝试执行。

核心签名

cpp 复制代码
// 位于 <mutex>
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
  • flag:必须是 std::once_flag 类型的实例(且不能拷贝)。

  • f:要执行的函数。

  • args:传递给函数的参数。

实战实例

经典场景:单例模式 或者初始化全局日志系统

cpp 复制代码
#include <mutex>
#include <iostream>
#include <thread>
#include <vector>

std::once_flag init_flag; // 必须是全局或静态的标志位
std::vector<int> shared_resource;

void init_resource() {
    // 模拟耗时的初始化操作
    std::cout << "正在初始化资源 (只应打印一次)...\n";
    shared_resource = {1, 2, 3, 4, 5};
}

void worker(int id) {
    // 无论多少线程运行到这,init_resource 都只会被执行一次
    std::call_once(init_flag, init_resource);
    
    // 初始化完成后,大家都可以安全使用资源了
    std::cout << "线程 " << id << " 正在使用资源大小: " << shared_resource.size() << "\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    std::thread t3(worker, 3);

    t1.join(); t2.join(); t3.join();
}

总结对照

|--------------------|---------------------|------------|-----------------------------|
| 函数 | 核心作用 | 返回值 | 关键用法 |
| std::lock | 死锁避免。阻塞直到拿到所有锁。 | void | 必须配合 adopt_lock 使用。 |
| std::try_lock | 全拿或全不拿。非阻塞尝试。 | int (-1成功) | 失败会自动回滚解锁;成功需配合 adopt_lock。 |
| std::call_once | 一次性初始化。 | void | 必须配合 std::once_flag。 |

三、 条件变量 <condition_variable>

用于线程间的等待和通知机制。

1. std::condition_variable

必须配合 std::unique_lock<std::mutex> 使用。

等待函数 (Wait)

  • wait(lock): 释放锁并挂起线程。

  • wait(lock, predicate): 推荐。等价于 while(!pred()) wait(lock);。防止虚假唤醒。

  • wait_for(lock, duration, [pred]): 等待一段时间。

  • wait_until(lock, time_point, [pred]): 等待直到某时刻。

通知函数 (Notify)

  • notify_one(): 唤醒一个正在等待的线程(随机选择)。

  • notify_all(): 唤醒所有正在等待的线程(惊群效应)。

2. std::condition_variable_any

更通用的版本,可以配合任何符合 Lockable 协议的锁(如自旋锁、读写锁),但效率略低于前者。

这是一个关于 C++ 线程同步中最核心机制------条件变量的详细讲解。条件变量主要用于解决"线程间的等待与通知"问题(例如:生产者-消费者模型)。

下面我将分两部分,详细讲解 std::condition_variable 和 std::condition_variable_any 的签名与实例。

|------------|-----------------------------------|-------------------------------------------------------------------------------|
| 特性 | std::condition_variable | std::condition_variable_any |
| 支持的锁类型 | 仅限 std::unique_lock<std::mutex> | 任意符合 BasicLockable 要求的锁 (如 std::shared_lock, std::recursive_mutex, 自定义锁等) |
| 性能/开销 | 较高(通常由操作系统原生支持,对象体积小) | 稍低(实现更复杂,体积可能更大,某些实现可能涉及堆内存分配) |
| 适用场景 | 90% 的常规多线程开发 | 特殊场景,特别是涉及读写锁时 |

std::condition_variable

它的主要作用是让一个线程等待(阻塞),直到另一个线程通知它某个条件已经满足。它是实现多线程之间协作(尤其是"生产者-消费者"模型)的核心工具。

这是最常用、性能最好的版本,但它强制要求必须配合 std::unique_lock<std::mutex> 使用。

核心函数签名

cpp 复制代码
// 头文件: <condition_variable>
class condition_variable {
public:
    // === 等待函数 ===
    
    // 1. 死等 (直到被 notify)
    void wait(std::unique_lock<std::mutex>& lock);
    
    // 2. [推荐] 条件等待 (防止虚假唤醒)
    // Predicate 是一个返回 bool 的函数或 Lambda
    // 等价于: while (!pred()) wait(lock);
    template<class Predicate>
    void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

    // 3. 超时等待 (相对时间)
    // 返回 cv_status::timeout 或 cv_status::no_timeout
    template<class Rep, class Period>
    std::cv_status wait_for(std::unique_lock<std::mutex>& lock, 
                            const std::chrono::duration<Rep, Period>& rel_time);
                            
    // 4. 超时等待 (绝对时间)
    template<class Clock, class Duration>
    std::cv_status wait_until(std::unique_lock<std::mutex>& lock, 
                              const std::chrono::time_point<Clock, Duration>& abs_time);

    // === 通知函数 ===
    
    // 唤醒一个等待线程
    void notify_one() noexcept;
    
    // 唤醒所有等待线程
    void notify_all() noexcept;
};

实战实例:生产者-消费者模型

这是条件变量最经典的使用场景。

  • 生产者:生产数据 -> 加锁 -> 放入队列 -> 解锁 -> notify_one 通知消费者。

  • 消费者:加锁 -> wait (等待队列不为空) -> 取出数据 -> 解锁 -> 处理数据。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool finished = false; // 结束标志

// 消费者线程
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lk(mtx);

        // 【关键点】 wait(锁, 谓词)
        // 含义:如果队列为空且任务没结束,我就释放锁并挂起等待。
        // 一旦被唤醒,先拿锁,再检查条件 (pred)。如果不满足,继续睡;满足则往下走。
        cv.wait(lk, []{ return !data_queue.empty() || finished; });

        // 此时已持有锁,且条件满足
        if (data_queue.empty() && finished) {
            break; // 队列空了且任务结束,退出
        }

        int data = data_queue.front();
        data_queue.pop();
        
        // 提早解锁,处理数据不需要占着锁
        lk.unlock(); 
        
        std::cout << "消费: " << data << std::endl;
    }
}

// 生产者线程
void producer() {
    for (int i = 0; i < 5; ++i) {
        {
            std::lock_guard<std::mutex> lk(mtx);
            data_queue.push(i);
            std::cout << "生产: " << i << std::endl;
        } // 离开作用域自动解锁
        
        // 通知一个正在 wait 的消费者
        // 注意:notify 不需要持有锁,放在解锁之后效率更高(避免刚唤醒消费者就发现锁还没释放)
        cv.notify_one(); 
        
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    // 生产结束
    {
        std::lock_guard<std::mutex> lk(mtx);
        finished = true;
    }
    cv.notify_all(); // 通知所有消费者该下班了
}

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

std::condition_variable_any

这个类更加灵活。它不要求 锁必须是 std::unique_lock<std::mutex>。

它可以配合任何拥有 lock() 和 unlock() 方法的对象,比如:

  • std::shared_lock<std::shared_mutex> (读写锁)

  • std::unique_lock<std::recursive_mutex> (递归锁)

  • 甚至是自定义的自旋锁。

核心签名

cpp 复制代码
// 头文件: <condition_variable>
class condition_variable_any {
public:
    // 注意:这里的 Lock 是模板参数,可以是任何锁类型
    template<class Lock>
    void wait(Lock& lock);

    template<class Lock, class Predicate>
    void wait(Lock& lock, Predicate pred);

    // wait_for, wait_until, notify_one, notify_all 同上
};

实战实例:配合读写锁 (shared_mutex)

假设我们有一个读者-写者场景,读者等待某个状态变更。因为使用了读写锁,普通的 condition_variable 无法配合 std::shared_lock 使用,这时必须用 condition_variable_any。

cpp 复制代码
#include <iostream>
#include <thread>
#include <shared_mutex> // C++17
#include <condition_variable>

std::shared_mutex rw_mtx; // 读写锁
std::condition_variable_any cv_any; // 通用条件变量
bool ready = false;

void reader(int id) {
    // 使用共享锁 (读锁)
    std::shared_lock<std::shared_mutex> lock(rw_mtx);
    
    // wait 接受 shared_lock
    // 当 wait 挂起时,它会释放这个读锁;被唤醒时,会重新获取读锁
    cv_any.wait(lock, []{ return ready; });
    
    std::cout << "读者 " << id << " 看到 ready 变为 true" << std::endl;
}

void writer() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        // 获取写锁
        std::unique_lock<std::shared_mutex> lock(rw_mtx);
        ready = true;
        std::cout << "写者将 ready 设为 true" << std::endl;
    }
    // 通知所有读者
    cv_any.notify_all();
}

int main() {
    std::thread r1(reader, 1);
    std::thread r2(reader, 2);
    std::thread w(writer);

    r1.join(); r2.join(); w.join();
}

总结对比

|----------|------------------------------------|-----------------------------|
| 特性 | std::condition_variable | std::condition_variable_any |
| 配合的锁 | 只能是 std::unique_lock<std::mutex> | 任何锁 (如 shared_lock, 自定义锁) |
| 性能 | (针对系统底层 API 优化) | 略低 (因为通用性增加了内部开销) |
| 内存占用 | 小 | 稍大 |
| 适用场景 | 90% 的常规场景 | 特殊场景 (如读写锁、递归锁) |

一句话建议 :除非你必须要用读写锁或其他特殊锁来 wait,否则永远优先使用 std::condition_variable

四、 未来与承诺 <future>

处理异步任务返回值的机制。

<future> 头文件主要解决了一个核心痛点:如何方便地从另一个线程那里拿到返回值?

普通的 std::thread 是"发射后不管(Fire and Forget)"的。虽然你可以传参数进去,但线程跑完后,它没法直接 return 一个值给你。你通常得用全局变量+锁+条件变量来倒腾数据,非常麻烦。

<future> 库就是为了解决这个问题而生的。它引入了两个核心概念:Future(未来)Promise(承诺)

披萨店模型

想象你去餐厅点餐:

Promise(承诺 / 厨师)

  • 你点完餐,后厨的厨师接单了。厨师承诺(Promise)一定会把披萨做出来,或者如果烤糊了会告诉你(抛出异常)。

  • 在代码中 :std::promise 是数据的生产者(设置值的一方)。

Future(未来 / 小票)

  • 服务员给了你一张小票(Future)。这张小票现在不能吃,但它代表了**"在未来某个时间点,你可以凭此换取一个真披萨"**。

  • 在代码中 :std::future 是数据的消费者(获取值的一方)。

流程

  1. 你拿着小票(future)去逛街(主线程做其他事)。

  2. 厨师(子线程)在做披萨。

  3. 当你饿了,你拿着小票去柜台(调用 future.get())。

  4. 如果披萨做好了,你立马拿走;如果没做好,你就得在柜台死等(阻塞),直到做好。

<future> 的三大优势

  1. 直接获取返回值

    再也不用定义全局变量,再也不用手动加锁去读那个变量了。future.get() 一步到位。

  2. 捕获异常

    这是 std::thread 做不到的。

    如果子线程里抛出了异常(比如 throw std::runtime_error("出错啦")),这个异常会通过 Future "传送" 到主线程。

    当你调用 future.get() 时,主线程会抛出同样的异常,你可以在主线程里 try-catch 住它。

  3. 同步机制

    future.wait() 和 future.get() 天然就是同步屏障。你不需要额外写 condition_variable 来等待结果就绪。

1. 提供者 (Providers - 生产结果)

std::promise<T>:

  • 手动设置值。

  • set_value(val): 设置结果。

  • set_exception(e): 设置异常。

  • get_future(): 获取关联的 future 对象。

std::packaged_task<T(Args...)>:

  • 包装一个函数,使其能异步执行。

  • operator(): 执行函数,并将结果存入内部状态。

  • get_future(): 获取关联的 future 对象。

std::async (函数):

  • 高级接口。启动一个异步任务。

  • 参数:std::launch::async (强制新线程) 或 std::launch::deferred (惰性执行)。

  • 返回:std::future<T>。

2. 接收者 (Receivers - 获取结果)

std::future<T>:

  • 独占。只能 get() 一次。

  • get(): 阻塞直到结果准备好,取出值(之后对象失效)。

  • wait(): 等待结果准备好。

  • wait_for(), wait_until(): 超时等待。

  • valid(): 检查目前是否关联有效状态。

  • share(): 转换为 shared_future。

std::shared_future<T>:

  • 共享。可以拷贝,可以多次 get()(返回 const 引用)。多个线程可以等待同一个结果。

提供者 (Providers - 生产结果)

这些类负责制造数据,并将其放入共享状态中。

1. std::promise<T>

作用:最底层的"承诺"。在线程 A 中创建一个"空槽",承诺将来在线程 B 中填入数据。

核心签名

cpp 复制代码
template< class T > class promise;

// 1. 设置结果 (值会存入共享状态,通知 future)
void set_value( const T& value );
void set_value( T&& value );

// 2. 设置异常 (如果任务失败,把异常填进去,future.get() 时会抛出这个异常)
void set_exception( std::exception_ptr p );

// 3. 获取关联的 future (只能调用一次)
std::future<T> get_future();

实例

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>

void worker(std::promise<int> p) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    // 任务完成,填入结果
    p.set_value(100); 
}

int main() {
    std::promise<int> p;
    // 1. 先拿到"提货券"
    std::future<int> f = p.get_future();

    // 2. 把 promise 移交给子线程 (promise 不能拷贝,只能 move)
    std::thread t(worker, std::move(p));

    std::cout << "等待结果...\n";
    // 3. 阻塞直到 set_value 被调用
    std::cout << "结果: " << f.get() << "\n";
    
    t.join();
}

2. std::packaged_task<T(Args...)>

作用 :包装一个可调用的函数,自动将函数的返回值连接到 future 上。比 promise 省事,不用手动 set_value。

核心签名

cpp 复制代码
// 模板参数是函数签名,例如 int(int, int)
template< class R, class... ArgTypes >
class packaged_task<R(ArgTypes...)>;

// 1. 获取 future
std::future<R> get_future();

// 2. 像函数一样调用它。执行后,返回值会自动填入 future
void operator()( ArgTypes... args );

实例

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>

// 一个普通函数
int add(int a, int b) { return a + b; }

int main() {
    // 1. 包装任务
    std::packaged_task<int(int, int)> task(add);

    // 2. 获取 future
    std::future<int> f = task.get_future();

    // 3. 放到线程里跑 (task 也是只支持 move)
    std::thread t(std::move(task), 10, 20);

    // 4. 获取结果
    std::cout << "10 + 20 = " << f.get() << "\n";

    t.join();
}

3. std::async (函数)

作用:最高级的封装。自动创建 promise,自动打包 task,自动(可选)创建线程。

核心签名

cpp 复制代码
// 这里的返回值是 future<函数返回值类型>
template< class Function, class... Args >
std::future<...> async( std::launch policy, Function&& f, Args&&... args );
  • policy 参数:

    • std::launch::async: 强制开启新线程。

    • std::launch::deferred: 惰性执行。只有当你调用 future.get() 时,才在当前线程执行任务(不创建新线程)。

实例

cpp 复制代码
#include <iostream>
#include <future>

int heavy_calc() { return 42; }

int main() {
    // 一行代码搞定异步任务
    std::future<int> f = std::async(std::launch::async, heavy_calc);

    std::cout << "主线程继续做事...\n";

    // 拿结果
    std::cout << "结果: " << f.get() << "\n";
}

接收者 (Receivers - 获取结果)

这些类负责持有"提货券",等待数据就绪。

1. std::future<T>

作用独占的提货券。一份数据只能被一个对象取走。一旦 get(),对象就空了。

核心签名

cpp 复制代码
template< class T > class future;

// 1. 取值 (阻塞直到有值)。调用后 future 变为空(invalid)
T get();

// 2. 等待 (不取值)
void wait() const;

// 3. 超时等待
// 返回 future_status::ready (好了), timeout (超时), deferred (还没开始)
std::future_status wait_for( duration );

// 4. 检查是否有效 (get 之后会变成 false)
bool valid() const noexcept;

// 5. 转换为共享 future
std::shared_future<T> share() noexcept;

实例

cpp 复制代码
std::future<int> f = std::async(std::launch::async, []{ return 10; });

if (f.valid()) {
    // 等待最多 1 秒
    if (f.wait_for(std::chrono::seconds(1)) == std::future_status::ready) {
        std::cout << f.get() << "\n"; // 数据被移出
    }
}

// 再次调用 valid() 将返回 false
// 再次调用 get() 会抛出异常

2. std::shared_future<T>

作用共享的提货券。可以把结果广播给多个线程。

核心签名

cpp 复制代码
template< class T > class shared_future;

// 1. 取值 (返回 const 引用,不会让对象失效)
// 可以被多次调用
const T& get() const;

// 2. 支持拷贝构造 (std::future 不支持拷贝,只支持移动)
shared_future( const shared_future& other );

实例

场景:主线程下载一个文件,三个子线程都需要用到这个文件的内容。

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>
#include <vector>

int download_data() { return 888; }

void analyze(int id, std::shared_future<int> sf) {
    // 多个线程都可以调用 get()
    std::cout << "线程 " << id << " 获取到数据: " << sf.get() << "\n";
}

int main() {
    std::promise<int> p;
    // 1. 先拿到普通 future,然后转为 shared
    // 或者直接用 p.get_future().share()
    std::shared_future<int> sf = p.get_future().share();

    // 2. 启动多个线程,sf 可以按值拷贝传递
    std::thread t1(analyze, 1, sf);
    std::thread t2(analyze, 2, sf);

    p.set_value(download_data()); // 数据广播给所有持有 sf 的线程

    t1.join();
    t2.join();
}

五、 原子操作 <atomic>

这是 C++ 并发编程中最硬核、最接近底层的部分。std::atomic 利用 CPU 的特殊指令(如 x86 的 LOCK 前缀或 ARM 的 LDREX/STREX)来保证操作的原子性,避免了互斥锁(Mutex)的昂贵开销。

1. std::atomic<T>

T 通常是整数、指针或 bool。

基础操作

  • store(val, order): 写入值。

  • load(order): 读取值。

  • exchange(val, order): 写入新值,返回旧值 (Atomic Swap)。

算术操作 (仅针对整数/指针)

  • fetch_add(val), fetch_sub(val): 加/减,返回修改的值。

  • fetch_and, fetch_or, fetch_xor: 位运算。

  • operator++, operator+= 等重载运算符。

比较与交换 (CAS - Compare And Swap)

这是无锁编程的核心。

  • compare_exchange_weak(expected, desired): 可能虚假失败(即便值相等也返回 false),循环中使用效率高。

  • compare_exchange_strong(expected, desired): 保证值相等时一定成功。

2. 内存序 (std::memory_order)

所有原子函数都可选一个 memory_order 参数,控制指令重排。

  • memory_order_relaxed: 松散(无同步,只保证当前原子性)。

  • memory_order_acquire: 获取(读屏障)。

  • memory_order_release: 释放(写屏障)。

  • memory_order_acq_rel: 获取+释放。

  • memory_order_seq_cst: 顺序一致性(默认,最慢但最安全)。

std::atomic<T> 基础与算术操作

T 通常是 int, long, bool 或指针类型。

1. 基础操作 (load, store, exchange)

这三个函数是最基本的读写操作。

函数签名

cpp 复制代码
// 读取
T load(std::memory_order order = std::memory_order_seq_cst) const noexcept;

// 写入
void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;

// 交换 (Read-Modify-Write):写入新值,并返回旧值
T exchange(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;

实例

cpp 复制代码
#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> g_data(0);
std::atomic<bool> g_ready(false);

void producer() {
    int data = 42;
    // 写入数据
    g_data.store(data, std::memory_order_release); 
    // 写入标志位
    g_ready.store(true, std::memory_order_release);
}

void consumer() {
    // 循环等待直到 ready 为 true
    // load 读取值
    while (!g_ready.load(std::memory_order_acquire)) {
        std::this_thread::yield();
    }
    // 读取数据
    std::cout << "Data: " << g_data.load(std::memory_order_acquire) << "\n";
}

// exchange 实例:原子地"抢占"锁
std::atomic<bool> locked(false);
void try_enter_critical_section() {
    // 尝试把 locked 设为 true。
    // 如果返回 false,说明之前没锁,我抢到了。
    // 如果返回 true,说明之前已经被锁了,我没抢到。
    if (!locked.exchange(true)) {
        std::cout << "我拿到了锁\n";
        // ... 干活 ...
        locked.store(false); // 释放锁
    }
}

2. 算术操作 (fetch_...)

这些操作仅适用于整型指针 类型的原子变量。它们是 Read-Modify-Write (RMW) 操作。

核心签名

cpp 复制代码
// 加法:将 val 加到当前值上,并返回【加之前】的旧值
T fetch_add(T val, std::memory_order order = std::memory_order_seq_cst) noexcept;

// 减法:返回【减之前】的旧值
T fetch_sub(T val, std::memory_order order = std::memory_order_seq_cst) noexcept;

// 位运算:与、或、异或
T fetch_and(T val, std::memory_order order = std::memory_order_seq_cst) noexcept;
T fetch_or(T val, std::memory_order order = std::memory_order_seq_cst) noexcept;
T fetch_xor(T val, std::memory_order order = std::memory_order_seq_cst) noexcept;

运算符重载

  • operator++() 等价于 fetch_add(1) + 1 (返回新值)

  • operator++(int) 等价于 fetch_add(1) (返回旧值)

  • operator+= 等价于 fetch_add 后返回新值

实例

cpp 复制代码
std::atomic<int> counter(0);

void worker() {
    // 原子加 1,多线程并发也不会算错
    // fetch_add 返回的是加之前的值 (例如 0),curr 变成 1
    int old_val = counter.fetch_add(1); 
    
    // operator++ 是语法糖
    counter++; 
}

// 实际应用:抢号系统
std::atomic<int> ticket_dispenser(0);
int take_ticket() {
    // 每个人拿到的号码绝对唯一,且连续
    return ticket_dispenser.fetch_add(1); 
}

3. 比较与交换 (CAS - Compare And Swap)

这是无锁编程(Lock-free Programming)的灵魂。几乎所有无锁数据结构(无锁队列、栈)都是基于 CAS 实现的。

逻辑

"我看一眼内存里的值是不是 expected(预期值)。

如果是,我就把它改成 desired(新值),并返回 true(成功)。

如果不是(说明被别人改过了),我就把内存里最新的值填到 expected 变量里,并返回 false(失败)。"

函数签名

cpp 复制代码
// 弱 CAS:允许虚假失败 (Spurious Failure)
// 即使值相等,偶尔也会返回 false (硬件原因)。适合写在 while 循环里。
bool compare_exchange_weak(T& expected, T desired, 
                           std::memory_order order = std::memory_order_seq_cst) noexcept;

// 强 CAS:保证值相等时一定成功
// 内部包含了循环重试逻辑,开销略大一点点。
bool compare_exchange_strong(T& expected, T desired, 
                             std::memory_order order = std::memory_order_seq_cst) noexcept;

实例 :如何实现一个线程安全的"最大值更新"?

(普通 if (val > max) max = val 在多线程下是不安全的,CAS 可以解决)

cpp 复制代码
std::atomic<int> max_value(0);

void update_max(int new_val) {
    // 1. 先读取当前的最大值作为"预期值"
    int prev_max = max_value.load();

    // 2. 循环尝试更新
    // 如果 prev_max < new_val,尝试把 max_value 更新为 new_val
    while (prev_max < new_val && 
           !max_value.compare_exchange_weak(prev_max, new_val)) {
        // 如果进入循环体,说明 CAS 失败了。
        // 原因 A:内存里的值被别人改了,不等于 prev_max。
        // 原因 B:虚假失败 (weak 特有)。
        
        // 【关键点】:compare_exchange_weak 失败时,
        // 会自动把内存里最新的值更新到 prev_max 变量里!
        // 所以下一次循环比较的就是最新的值。
    }
}

内存序 (std::memory_order)

这是 C++ 为了极致性能暴露给程序员的"指令重排控制权"。如果不指定,默认都是 memory_order_seq_cst(最安全但最慢)。

1. memory_order_relaxed (松散)

  • 作用 :只保证这一步操作是原子的。不保证顺序

  • 场景:单纯的计数器,不涉及线程间数据同步。

cpp 复制代码
std::atomic<int> cnt = {0};
void f() {
    // 随便乱序执行都行,只要最终加了1即可
    cnt.fetch_add(1, std::memory_order_relaxed);
}

2. memory_order_acquire (获取 - 读屏障)

  • 作用 :用于 load (读) 操作。

  • 含义"在这个操作之后的读写指令,绝对不允许被重排到这个操作之前。"

  • 场景 :我看一眼红绿灯(acquire),看到绿灯了,然后我才能踩油门(后续操作)。

3. memory_order_release (释放 - 写屏障)

  • 作用 :用于 store (写) 操作。

  • 含义"在这个操作之前的读写指令,绝对不允许被重排到这个操作之后。"

  • 场景 :我先把货装好(前置操作),然后关上卡车门(release)。不能先关门再装货。

4. memory_order_acq_rel (获取+释放)

  • 作用 :用于 Read-Modify-Write (RMW) 操作(如 fetch_add, exchange)。

  • 含义:同时具有上述两者的屏障效果。

5. memory_order_seq_cst (顺序一致性 - 默认)

  • 作用:最强约束。全局所有线程看到的原子操作顺序都是一致的。

  • 代价:禁止了 CPU 很多优化,性能最低。

实战:Acquire-Release 模型 (最经典用法)

这是替代 mutex 进行线程同步的标准写法。
线程 A 准备数据 -> 线程 A 发布数据 -> 线程 B 获取数据 -> 线程 B 读取数据

cpp 复制代码
#include <atomic>
#include <thread>
#include <string>
#include <cassert>

std::string data;
std::atomic<bool> ready(false);

void producer() {
    // 1. 准备数据 (非原子)
    data = "Hello World"; 
    
    // 2. 发布 (Release)
    // 保证 Step 1 的写入绝对不会跑到 Step 2 后面去
    ready.store(true, std::memory_order_release); 
}

void consumer() {
    // 3. 获取 (Acquire)
    // 保证 Step 4 的读取绝对不会跑到 Step 3 前面去
    while (!ready.load(std::memory_order_acquire)) {
        ; // 等待
    }
    
    // 4. 读取数据 (非原子)
    // 此时 data 必定已经写入完成
    assert(data == "Hello World"); 
}

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

总结

  1. 小白/业务开发:直接用默认的 std::memory_order_seq_cst(也就是不传参数)。虽然慢点,但绝对不出错。

  2. 高性能开发:如果是单纯计数器,用 relaxed。

  3. 库开发者/底层同步:使用 acquire 和 release 配合来实现"发布-订阅"模式。

  4. CAS:用 compare_exchange_weak 配合 while 循环是标准写法。

六、 C++20 现代并发设施

这是 C++20 对并发编程的一次重大升级,旨在解决 C++11 遗留的痛点(如手动 join 容易崩、缺乏轻量级同步原语等)。

1. std::jthread (Header: <thread>)

自动汇合:析构时自动调用 request_stop() 和 join()。

协作式停止

  • 构造时自动传入 std::stop_token 给线程函数。

  • request_stop(): 发出停止请求。

  • stop_token: 线程函数内部调用 st.stop_requested() 检查是否该退出了。

2. 信号量 (Header: <semaphore>)

std::counting_semaphore<Max>: 计数信号量。

  • acquire(): 计数减 1(若为 0 则阻塞)。

  • release(n): 计数加 n(唤醒等待者)。

  • try_acquire(): 尝试减 1。

std::binary_semaphore: 二值信号量(特化版,Max=1),类似互斥锁,但更轻量。

3. 门闩 (Header: <latch>)

一次性同步点,计数器减到 0 后门打开,不可重置。

  • std::latch(count): 构造。

  • count_down(n): 计数减 n。

  • wait(): 等待直到计数为 0。

  • arrive_and_wait(): 减 1 并等待。

4. 屏障 (Header: <barrier>)

可循环使用的同步点。一组线程全部到达后,执行回调,然后进入下一轮。

  • std::barrier(count, completion_func): 构造。

  • arrive_and_wait(): 到达屏障并等待其他人。

  • arrive_and_drop(): 我退出了,以后别等我了(计数减 1)。

std::jthread (自动汇合与协作停止)

它是 std::thread 的现代化封装。两个核心改进:

  1. RAII:析构时自动请求停止并 join(再也不用担心忘记 join 导致 std::terminate 崩溃)。

  2. 内置停止机制:不需要自己定义 atomic<bool> 标志位来通知线程退出了。

cpp 复制代码
// 头文件: <thread>
class jthread {
public:
    // 构造函数:支持自动传递 std::stop_token 作为函数的第一个参数
    template<class Fn, class... Args>
    explicit jthread(Fn&& fn, Args&&... args);

    // 发出停止请求 (把内部的 stop_source 设为 stop 状态)
    bool request_stop() noexcept;

    // 手动 join (通常不需要调用,析构会自动调)
    void join();
    
    // 获取停止令牌 (用于传给其他函数)
    std::stop_token get_stop_token() const noexcept;
};

场景:一个后台线程每秒打印一次日志,主线程运行 3 秒后让它停下。

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>

using namespace std::chrono_literals;

// 注意:这里参数接收 stop_token,是 C++20 的魔法
// jthread 会自动把它传进来
void worker(std::stop_token st) {
    // 循环检查是否收到了停止请求
    while (!st.stop_requested()) {
        std::cout << "Worker is running...\n";
        std::this_thread::sleep_for(1s);
    }
    std::cout << "Worker received stop request, exiting.\n";
}

int main() {
    std::cout << "Main starts\n";
    {
        // 创建 jthread
        std::jthread t(worker);
        
        // 主线程睡 3 秒
        std::this_thread::sleep_for(3.5s);
        
        // 离开作用域时:
        // 1. t.request_stop() 被自动调用 -> st.stop_requested() 变为 true
        // 2. t.join() 被自动调用 -> 等待 worker 结束
    }
    std::cout << "Main ends\n";
}

信号量 (std::semaphore)

信号量本质上是一个计数器,用来控制同时访问某个资源的线程数量。

  • Counting Semaphore: 停车场模型(有 N 个车位)。

  • Binary Semaphore: 互斥锁模型(只有 1 个车位)。

cpp 复制代码
// 头文件: <semaphore>

// 模板参数 LeastMaxValue 是计数的最大值
template<std::ptrdiff_t LeastMaxValue = std::numeric_limits<std::ptrdiff_t>::max()>
class counting_semaphore {
public:
    // 构造函数,设置初始计数
    explicit counting_semaphore(std::ptrdiff_t desired);

    // P操作:计数减 1。如果计数为 0,则阻塞等待
    void acquire();

    // V操作:计数加 update。唤醒正在等待的线程
    void release(std::ptrdiff_t update = 1);

    // 非阻塞尝试减 1
    bool try_acquire();
    
    // 超时尝试
    bool try_acquire_for(Duration...);
};

// 二值信号量 (特化版)
using binary_semaphore = std::counting_semaphore<1>;

场景:限制并发数量。假设有一个下载任务,只允许最多 3 个线程同时下载。

cpp 复制代码
#include <iostream>
#include <thread>
#include <semaphore>
#include <vector>
#include <chrono>

// 定义一个最多允许 3 个并发的信号量
std::counting_semaphore<3> download_sem(3);

void downloader(int id) {
    std::cout << "Thread " << id << " waiting in queue...\n";
    
    // P操作:申请资源。如果此时已有 3 人在下载,这里会阻塞
    download_sem.acquire();
    
    std::cout << "Thread " << id << " start downloading! [Slot Acquired]\n";
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟下载
    
    std::cout << "Thread " << id << " finished. [Slot Released]\n";
    
    // V操作:释放资源。让排队的人进来
    download_sem.release();
}

int main() {
    std::vector<std::thread> threads;
    // 启动 10 个线程,但你会发现同时只有 3 个在跑
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(downloader, i);
    }

    for (auto& t : threads) t.join();
}

门闩 (std::latch)

一次性的倒计时器。倒计时归零后,门闩永久打开。通常用于初始化同步。

cpp 复制代码
// 头文件: <latch>
class latch {
public:
    // 构造函数,设置倒计时初始值
    explicit latch(std::ptrdiff_t expected);

    // 计数减 n,如果不传 n 默认减 1
    void count_down(std::ptrdiff_t n = 1);

    // 阻塞等待,直到计数变为 0
    void wait() const;

    // 组合拳:先减 n,然后立刻等待直到 0 (常用)
    void arrive_and_wait(std::ptrdiff_t n = 1);
    
    // 检查是否已归零
    bool try_wait() const noexcept;
};

场景:老板(主线程)等待 5 个员工(子线程)全部加载完数据,才能宣布项目开始。

cpp 复制代码
#include <iostream>
#include <thread>
#include <latch>
#include <vector>

// 倒计时门闩,初始值为 5
std::latch worker_done(5);

void worker(int id) {
    // 模拟初始化工作
    std::cout << "Worker " << id << " loading data...\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(100 * id));
    
    std::cout << "Worker " << id << " done.\n";
    // 计数减 1。注意:这里不需要阻塞,减完就没事了
    worker_done.count_down();
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 1; i <= 5; ++i) {
        threads.emplace_back(worker, i);
    }

    std::cout << "Main thread waiting for workers...\n";
    
    // 主线程阻塞在这里,直到 count_down 被调用 5 次
    worker_done.wait();
    
    std::cout << "All workers ready! Project Start!\n";

    for (auto& t : threads) t.join();
}

屏障 (std::barrier)

可循环使用的同步点。所有线程到达屏障后,执行一个回调(可选),然后屏障重置,进入下一轮。

cpp 复制代码
// 头文件: <barrier>
template<class CompletionFunction = /*...*/ >
class barrier {
public:
    // 构造函数:预期到达数量 + 到齐后的回调函数
    constexpr explicit barrier(std::ptrdiff_t expected, 
                               CompletionFunction completion = CompletionFunction());

    // 到达屏障并等待其他人。等所有人齐了,进入下一轮。
    void arrive_and_wait();

    // 到达屏障,但之后我退出了,把预期数量减 1。
    void arrive_and_drop();
};

场景:3 个线程并行计算,分为 Phase 1 和 Phase 2。必须所有人都做完 P1,才能一起进 P2。

cpp 复制代码
#include <iostream>
#include <thread>
#include <barrier>
#include <vector>

// 定义一个屏障,需要 3 个线程到达
// 第二个参数是 Lambda,当 3 个人都到了之后,由其中一个线程执行这个打印
std::barrier sync_point(3, []() noexcept {
    std::cout << ">>> Phase Completed! Moving to next phase... <<<\n";
});

void worker(int id) {
    // --- 第一阶段 ---
    std::cout << "Thread " << id << " doing Phase 1...\n";
    std::this_thread::sleep_for(std::chrono::milliseconds(500 * id));
    
    // 等待其他人做完 Phase 1
    std::cout << "Thread " << id << " waiting at barrier 1...\n";
    sync_point.arrive_and_wait(); 

    // --- 第二阶段 (只有所有人都过了上面那行代码,才会执行这里) ---
    std::cout << "Thread " << id << " doing Phase 2...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    // 等待其他人做完 Phase 2
    sync_point.arrive_and_wait();
    
    std::cout << "Thread " << id << " finished.\n";
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 1; i <= 3; ++i) {
        threads.emplace_back(worker, i);
    }
    for (auto& t : threads) t.join();
}

总结区别:Latch vs Barrier

  • Latch (门闩) :是一次性的。像赛跑的发令枪,响了之后(归零),大家就跑了,门闩就废了。主要用于初始化

  • Barrier (屏障) :是循环的。像旅游团集合,早上的景点逛完(Phase 1),大家在大巴车前集合 (Barrier),人齐了发车去下午的景点(Phase 2)。主要用于分阶段并行计算

相关推荐
不绝1912 小时前
C#核心:多态
开发语言·c#
浪扼飞舟2 小时前
C#(多线程和同步异步)
java·开发语言
万行2 小时前
机器人系统SLAM讲解
开发语言·python·决策树·机器学习·机器人
抬头望远方2 小时前
【无人机】无人机群在三维环境中的碰撞和静态避障仿真(Matlab代码实现)
开发语言·支持向量机·matlab·无人机
matlab科研助手2 小时前
【路径规划】基于遗传算法的农药无人机在多边形区域的路径规划研究附Matlab代码
开发语言·matlab·无人机
2301_780669862 小时前
字符集及其编码、解码操作、IO流分类
java·开发语言
无名的小三轮2 小时前
第三章 防火墙概述
开发语言·php
有梦想的攻城狮2 小时前
Java中的Double类型的存在精度丢失详解
java·开发语言·bigdecimal·double
tod1132 小时前
从零手写一个面试级 C++ vector:内存模型、拷贝语义与扩容策略全解析
c++·面试·职场和发展·stl·vector