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