好的,各位观众,欢迎来到今天的C++线程管理脱口秀!今天咱们要聊的是C++20里面一位重量级选手——std::jthread
。这玩意儿啊,解决了C++多线程编程里一个老大难的问题:线程忘记join导致的程序崩溃。
开场白:线程的爱恨情仇
在C++11引入std::thread
之后,多线程编程的大门算是彻底打开了。但是,开门容易进门难,进门之后发现坑更多。最常见的一个坑就是线程对象的生命周期管理。如果你创建了一个std::thread
对象,并且线程还在跑,而std::thread
对象就被销毁了,那程序就等着崩溃吧!
为啥呢?因为std::thread
的析构函数会检查线程是否还在joinable状态。如果还在,它就会调用std::terminate()
直接结束程序。这就像你租了个房子,结果房东在你还没搬走的时候就把房子给拆了,你不得跟房东拼命?
所以,使用std::thread
的时候,你必须手动调用join()
或者detach()
来处理线程的生命周期。join()
就是等着线程跑完,detach()
就是让线程自己跑,跟主线程脱离关系。但是,程序员也是人,是人就会犯错。万一忘了join()
或者detach()
,那就是一场灾难。
std::jthread
:拯救世界的英雄
C++20为了拯救我们这些手残党,推出了std::jthread
。j
代表啥?joining
!也就是说,std::jthread
在析构的时候会自动join()
它管理的线程。这就像一个负责任的房东,在你搬走之前绝不会拆房子,而是会耐心等你处理完所有事情。
std::jthread
的基本用法
std::jthread
的用法和std::thread
差不多,但是更安全、更方便。咱们先来看一个简单的例子:
#include <iostream>
#include <thread>
#include <chrono>
void worker_thread(int id) {
std::cout << "线程 " << id << " 正在运行...n";
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "线程 " << id << " 运行结束。n";
}
int main() {
std::jthread t1(worker_thread, 1);
std::jthread t2(worker_thread, 2);
std::cout << "主线程正在运行...n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "主线程运行结束。n";
return 0;
}
在这个例子中,我们创建了两个std::jthread
对象t1
和t2
,分别执行worker_thread
函数。主线程也睡了一秒钟。当main
函数结束的时候,t1
和t2
会被销毁,它们的析构函数会自动调用join()
,等待线程执行完毕。这样,我们就不用担心线程忘记join()
的问题了。
std::jthread
的优势
- 自动
join()
: 这是std::jthread
最大的优势,也是它最吸引人的地方。妈妈再也不用担心我忘记join()
了! - 支持中断:
std::jthread
可以被中断。这意味着你可以优雅地停止一个正在运行的线程,而不是粗暴地结束它。 - 传递
std::stop_token
:std::jthread
的构造函数可以接受一个std::stop_token
参数。这个stop_token
可以用来检查线程是否被请求停止。
std::stop_token
:优雅地停止线程
std::stop_token
是C++20引入的一个新特性,用于请求线程停止执行。std::jthread
可以接收一个std::stop_token
,并在线程函数中使用它来检查是否需要停止。
#include <iostream>
#include <thread>
#include <chrono>
#include <stop_token>
void worker_thread_with_stop_token(std::stop_token stop_token, int id) {
std::cout << "线程 " << id << " 正在运行...n";
while (!stop_token.stop_requested()) {
std::cout << "线程 " << id << " 正在工作中...n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "线程 " << id << " 收到停止请求,即将退出。n";
}
int main() {
std::jthread t(worker_thread_with_stop_token, 1);
std::cout << "主线程正在运行...n";
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "主线程请求停止线程。n";
t.request_stop();
std::cout << "主线程运行结束。n";
return 0;
}
在这个例子中,worker_thread_with_stop_token
函数接收一个std::stop_token
参数。它使用stop_token.stop_requested()
来检查是否被请求停止。主线程在运行2秒后,调用t.request_stop()
来请求停止线程。线程收到停止请求后,会优雅地退出。
std::jthread
的构造函数
std::jthread
提供了多个构造函数,可以满足不同的需求:
构造函数 | 描述 |
---|---|
std::jthread() |
创建一个空的std::jthread 对象,不关联任何线程。 |
std::jthread(Fn&& fn, Args&&... args) |
创建一个std::jthread 对象,并启动一个线程执行fn(std::forward<Args>(args)...) 。 |
std::jthread(std::stop_token stoken, Fn&& fn, Args&&... args) |
创建一个std::jthread 对象,并启动一个线程执行fn(stoken, std::forward<Args>(args)...) 。线程函数可以接收一个std::stop_token 参数,用于检查是否被请求停止。 |
std::jthread(const std::jthread& other) |
拷贝构造函数,但是被删除。std::jthread 对象不能被拷贝。 |
std::jthread(std::jthread&& other) noexcept |
移动构造函数,将other 对象管理的线程的所有权转移给新的std::jthread 对象。other 对象不再管理任何线程。 |
std::jthread
的成员函数
成员函数 | 描述 |
---|---|
join() |
等待线程执行完毕。如果线程已经执行完毕,则立即返回。 |
detach() |
将线程与std::jthread 对象分离。线程将继续独立运行,不再受std::jthread 对象管理。 |
get_id() |
返回线程的ID。 |
native_handle() |
返回底层线程的句柄。 |
hardware_concurrency() |
返回硬件并发线程数。这个函数是一个静态成员函数。 |
joinable() |
检查线程是否是可joinable的。如果线程已经执行完毕或者已经被detach,则返回false ,否则返回true 。 |
request_stop() |
请求停止线程。这会设置std::stop_token 的停止标志。 |
get_stop_token() |
返回与std::jthread 对象关联的std::stop_token 对象。 |
swap(std::jthread& other) |
交换两个std::jthread 对象所管理的线程的所有权。 |
std::jthread
与std::thread
的比较
特性 | std::thread |
std::jthread |
---|---|---|
自动join() |
否 | 是 |
支持中断 | 否 | 是 |
构造函数 | 多个 | 多个 |
移动构造 | 支持 | 支持 |
拷贝构造 | 支持(C++11),但通常不建议 | 删除 |
std::jthread
的注意事项
- 异常处理: 即使
std::jthread
会自动join()
,你也需要注意线程函数中的异常处理。如果线程函数抛出异常,并且没有被捕获,那么程序仍然会崩溃。 - 死锁: 使用
std::jthread
并不能完全避免死锁的发生。如果多个线程互相等待对方释放资源,那么仍然会发生死锁。 detach()
: 虽然std::jthread
会自动join()
,但是你仍然可以调用detach()
来分离线程。但是,一旦你调用了detach()
,std::jthread
对象就不再管理线程的生命周期了,你需要自己负责确保线程在程序结束前执行完毕。
一个更复杂的例子:生产者-消费者模型
咱们来一个更复杂的例子,使用std::jthread
和std::stop_token
来实现一个简单的生产者-消费者模型。
#include <iostream>
#include <thread>
#include <chrono>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <stop_token>
// 共享数据
std::queue<int> queue;
std::mutex mutex;
std::condition_variable cv;
// 生产者线程
void producer(std::stop_token stop_token) {
int i = 0;
while (!stop_token.stop_requested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock(mutex);
queue.push(i++);
std::cout << "生产者生产了: " << i - 1 << std::endl;
lock.unlock();
cv.notify_one(); // 通知消费者
}
std::cout << "生产者收到停止请求,即将退出。n";
}
// 消费者线程
void consumer(std::stop_token stop_token) {
while (!stop_token.stop_requested()) {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&]() { return !queue.empty() || stop_token.stop_requested(); }); // 等待队列不为空或者收到停止请求
if (stop_token.stop_requested() && queue.empty()) {
std::cout << "消费者收到停止请求,队列为空,即将退出。n";
return;
}
int data = queue.front();
queue.pop();
std::cout << "消费者消费了: " << data << std::endl;
lock.unlock();
}
std::cout << "消费者收到停止请求,即将退出。n";
}
int main() {
std::jthread producer_thread(producer);
std::jthread consumer_thread(consumer);
std::cout << "主线程正在运行...n";
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "主线程请求停止生产者和消费者。n";
producer_thread.request_stop();
consumer_thread.request_stop();
std::cout << "主线程运行结束。n";
return 0;
}
在这个例子中,生产者线程不断地向队列中添加数据,消费者线程不断地从队列中取出数据。主线程在运行5秒后,会请求停止生产者和消费者线程。生产者和消费者线程都会检查std::stop_token
的停止标志,并在收到停止请求后优雅地退出。
总结:std::jthread
,多线程编程的得力助手
std::jthread
是C++20中一个非常实用的特性,它可以帮助我们更安全、更方便地进行多线程编程。它自动join()
的特性可以避免线程忘记join()
导致的程序崩溃,std::stop_token
可以让我们优雅地停止线程。如果你正在使用C++20,那么std::jthread
绝对值得你尝试。
记住,std::jthread
虽然强大,但也不是万能的。你仍然需要注意线程函数中的异常处理,避免死锁的发生。只有掌握了多线程编程的基本知识,才能真正发挥std::jthread
的威力。
好了,今天的脱口秀就到这里。感谢大家的收看,咱们下期再见!希望大家以后写多线程程序,都能像用std::jthread
一样轻松愉快!