C++20 并发新特性:std::jthread、stop_token 与 semaphore 改进

说实话,C++20 在并发这块儿,终于补齐了不少让人头疼的短板。我最早用 C++11 的 std::thread 时,最烦的就是忘记 join 或 detach,程序一崩就是半天。后来 C++20 来了,std::jthread 的出现,说白了就是帮你自动收拾烂摊子。再加上 stop_token 这套协作式取消机制,以及 counting_semaphore 的改进,嗯,写并发代码终于有点现代语言的样子了。

std::jthread:自动 join,告别资源泄漏

先聊聊 std::jthread。它和 std::thread 最大的区别是什么?你想想看,std::thread 如果析构时还没 join 或 detach,程序直接 terminate。我早期项目里就踩过这个坑——一个异常抛出来,线程对象没来得及处理,整个进程就挂了。后来我养成了习惯,每次创建线程都小心翼翼地用 RAII 包装,但说实话,挺烦的。

std::jthread 解决了这个问题。它的析构函数会自动调用 join(),确保线程执行完毕。而且它还内置了 stop_token 的支持,后面会细说。来看个简单例子:

#include <thread>
#include <iostream>

void worker() {
    std::cout << "Working..." << std::endl;
}

int main() {
    std::jthread t(worker);
    // 不用手动 join,t 析构时自动 join
    return 0;
}

你看,代码干净多了。我个人习惯是,只要不是特殊场景(比如需要 detach 的后台任务),一律用 std::jthread。它带来的安全感,就像你写代码时有个靠谱的同事帮你检查边界条件一样。

小提示: std::jthread 的析构函数会先请求停止(request_stop),再 join。这意味着如果你在 worker 里检查了 stop_token,线程可以优雅退出,而不是被强行中断。

std::stop_token:协作式取消,优雅地停下来

说到 stop_token,这是 C++20 里我最喜欢的新特性之一。以前想取消一个线程,要么用原子变量做标志位,要么用条件变量加复杂的状态机。我曾在项目中维护过一个老旧的线程池,取消逻辑散落在七八个文件里,每次改都提心吊胆。

stop_token 提供了一套标准化的协作式取消机制。注意我说的是「协作式」——它不是强制杀死线程,而是给线程一个信号,让它自己决定何时、如何退出。这样能避免资源泄漏和数据竞争。

基本用法是这样的:

#include <thread>
#include <iostream>
#include <chrono>

void worker(std::stop_token st) {
    while (!st.stop_requested()) {
        std::cout << "Working..." << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    std::cout << "Worker stopped gracefully." << std::endl;
}

int main() {
    std::jthread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(2));
    t.request_stop();  // 发出停止请求
    return 0;
}

这里的关键是 stop_requested() 检查。线程在每次循环中主动检查这个标志,如果为 true,就清理资源然后退出。我曾经在一个网络库中用过类似模式——当服务端收到关闭信号时,所有工作线程在完成当前请求后自动退出,不会丢数据。

注意: stop_token 是协作式的,不是强制终止。如果你的线程卡在阻塞调用(比如无超时的条件变量 wait)上,stop_token 可能无法立即生效。这时候需要结合 stop_callback 或带超时的 wait_for 来使用。

另外,stop_token 还支持回调注册。你可以通过 std::stop_callback 在停止请求发生时执行特定操作:

std::stop_source source;
std::stop_token st = source.get_token();

std::stop_callback cb(st, []{
    std::cout << "Stop requested! Cleaning up..." << std::endl;
});

// 稍后...
source.request_stop();  // 会触发回调

这个机制在资源清理场景下特别有用。比如你有一个线程在等待网络数据,收到停止信号后,你可以通过回调关闭 socket,让阻塞的 recv 返回错误,从而退出循环。

std::counting_semaphore:更灵活的同步原语

最后聊聊 counting_semaphore。C++20 之前,信号量是标准库的缺失一环。虽然你可以用条件变量模拟,但说实话,那代码写出来又长又容易出错。我见过不少项目自己封装信号量,结果边界条件处理得千疮百孔。

C++20 的 counting_semaphore 提供了 acquire、release、try_acquire 等操作,用法直观。它还有一个最大值的模板参数,用来限制并发访问数。来看个典型的生产者-消费者例子:

#include <semaphore>
#include <thread>
#include <queue>
#include <iostream>

std::counting_semaphore<5> sem(3);  // 最多允许3个并发访问
std::queue<int> data_queue;
std::mutex mtx;

void producer() {
    for (int i = 0; i < 10; ++i) {
        {
            std::lock_guard lk(mtx);
            data_queue.push(i);
            std::cout << "Produced: " << i << std::endl;
        }
        sem.release();  // 增加信号量计数,通知消费者
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    for (int i = 0; i < 10; ++i) {
        sem.acquire();  // 等待信号量
        std::lock_guard lk(mtx);
        int val = data_queue.front();
        data_queue.pop();
        std::cout << "Consumed: " << val << std::endl;
    }
}

int main() {
    std::jthread t1(producer);
    std::jthread t2(consumer);
    return 0;
}

这里 sem(3) 表示初始有 3 个资源可用。生产者每生产一个数据就 release 一次,消费者每次 acquire 消耗一个资源。如果资源数为 0,acquire 会阻塞直到有 release 发生。

相比 C++11 的条件变量方案,信号量的代码更短、意图更清晰。而且 counting_semaphore 的性能通常更好,因为它底层可能用原子操作实现,避免了条件变量的锁开销。

核心要点:
  • std::jthread 自动 join,减少资源泄漏风险
  • std::stop_token 提供协作式取消,线程可优雅退出
  • std::counting_semaphore 简化同步逻辑,性能优于条件变量

这三个特性放在一起,C++20 的并发编程体验提升了一个档次。我个人建议,新项目尽量用 C++20 标准,尤其是涉及多线程的部分。你想想看,少写一堆 join 和标志位代码,不仅省时间,还少了很多潜在的 bug。

最后,用一张图总结本章的知识体系:

C++20 并发新特性 std::jthread 自动 join 析构 内置 stop_token 支持 RAII 风格管理 std::stop_token 协作式取消 stop_callback 回调 优雅退出 counting_semaphore acquire / release 最大并发数控制 性能优于条件变量 目标:更安全、更优雅、更高效的并发编程

嗯,以上就是 C++20 在并发方面的三个重要改进。从自动管理的线程生命周期,到协作式的取消机制,再到更高效的同步原语,每一步都在降低我们写并发代码的心智负担。如果你还在用 C++17 或更早的标准,我建议你尽快升级——这些新特性真的能帮你省下不少调试时间。

公众号:蓝海资料掘金营,微信deep3321