随着时代的发展,你所碰到过的代码问题,不钻牛角尖的扯前沿算法之类,可以说99%以上,总有人碰到过。
对于我这种代码的小白来说,google在平时的工作中,真的不可或缺,通过google,只要具备合适的搜索技巧,可以说90%以上的具体一点的编码问题,你都可以找到参考,而且是那种成熟度很高的参考。
关于std::jthread,也是在项目开发中经高手提点,然后才关注到。
以下内容来自: C++ | std::jthread and cooperative cancellation with stop token - nextptr
概述
std ::jthread实例表示一个自动加入且可协作取消的线程。与std::thread相比, std::jthread具有异常安全的线程终止流程,并且可以在大多数情况下替换它,只需很少或无需更改代码。在详细介绍之前先进行快速比较:
void comparison() {
{ // Block
//std::thread
std::thread t([]() {
std::this_thread::sleep_for(1s);
});
//Must be joined explicitly (unless detached).
//Otherwise t.~thread() calls std::terminate()
t.join();
}
{
//std::jthread
std::jthread jt([]() {
std::this_thread::sleep_for(1s);
});
//Joins automatically in jt.~jthread()
}
}
为什么是std::jthread?
std ::thread封装了特定于操作系统的线程对象(例如,POSIX 系统上的pthread ),该对象应在终止时加入,以便操作系统可以回收与该线程关联的资源。线程可以被标记为分离——在这种情况下,操作系统会自动回收其资源。分离的线程不可连接。
std ::thread实例可以处于可连接或不可连接状态。默认构造、分离或移动的 std::thread 是不可连接的。我们必须在其生命结束之前显式地加入可连接的 std::thread;否则,std::thread的析构函数调用std::terminate,其默认行为是中止进程。
因此, std::thread可能会出现意外且难以排除故障的崩溃:
void unsafe() {
std::thread th([](){
//Some work...
});
/* ..more code...
An exception can be raised here!!
If exception is raised here,
th.~thread() will invoke std::terminate()
*/
th.join();
}
我们可以使用try-catch块使上述代码异常安全。但这只是许多此类可能性的一种特定模式,它强调了std::thread没有提供令人满意的RAII。即使(移动)分配给可连接的std::thread也会终止进程。
如果线程可连接,std::jthread通过自动加入析构函数(以及移动赋值)来解决此问题。
然而,有些人可能认为显式不调用 join 是一个编程错误,并且希望应用程序在这种情况下终止。在析构函数中加入提供了异常安全性,但可能会导致意外冻结,因为 join ()是等待另一个线程结束的阻塞调用。因此,std::jthread在销毁期间加入之前也会向线程发出停止请求。这是std::jthread析构函数的典型实现:
~jthread() {
if(joinable()) {
request_stop(); //More on stop request below.
join();
}
}
礼貌地请求线程停止而不是杀死它称为协作取消,我们将在下一节中介绍。
合作取消
C++20 还引入了基于取消标记的线程协作取消框架。从概念上讲,线程的顶级函数从原始函数接收取消标记。线程操作侦听令牌上的取消请求。发起函数或对象可以随时使用令牌来请求线程停止。
大多数线程平台(例如pthreads)支持几种中断线程的方法,从突然终止线程到礼貌地要求线程停止。但合作或礼貌的取消是唯一有效的方法。请参阅 Herb Sutter 的文章:礼貌地打断。
为了清晰区分,接收停止通知和请求停止的功能分为两种类型 - std::stop_token和std::stop_source。可以查询std::stop_token实例以获取停止通知。并且,std::stop_source提供了获取std::stop_token实例并向其关联令牌发送停止通知的函数。一个例子如下:
void data_processors() {
using namespace std::literals::chrono_literals;
//Create a stop source
std::stop_source source;
//Data processor thread function (lambda)
// gets a stop token from the stop source.
std::thread
processor1([stoken=ssource.get_token()]() {
while(!stoken.stop_requested()) {
//Process data...
std::this_thread::sleep_for(1s);
}
});
//Data processor thread function (lambda)
// gets a stop token from the stop source.
std::thread
processor2([stoken=ssource.get_token()]() {
while(!stoken.stop_requested()) {
//Process data...
std::this_thread::sleep_for(1s);
}
});
//Sleep for few seconds
std::this_thread::sleep_for(5s);
//Request stop. This would stop both data processors.
ssource.request_stop();
//Join the data processor threads
processor1.join();
processor2.join();
}
如上所示,原始函数(data_processors)拥有std::stop_source。它将std::stop_token传递给来自同一停止源的每个处理器操作。然后,它通过源向两个令牌发送停止请求。这两个操作都会主动监控其令牌,并根据请求停止。
然而,线程不能总是主动监视停止令牌。例如,等待条件变量的线程无法检查停止条件,除非收到信号通知。因此,通过std::stop_callback提供了回调机制。std ::stop_callback实例为给定的停止标记注册回调函数。当令牌收到停止请求时调用回调。
以下示例显示了如何使用std::stop_callback向等待停止请求的std::condition_variable 的线程发出信号:
using namespace std::literals::chrono_literals;
std::queue<int> jobs;
std::mutex mut;
std::condition_variable cv;
void worker(std::stop_token stoken) {
//Register a stop callback
std::stop_callback cb(stoken, []() {
cv.notify_all(); //Wake thread on stop request
});
while (true) {
int jobId = -1;
{ //Aquire a jobId in lock
std::unique_lock lck(mut);
cv.wait(lck, [stoken]() {
//Condition for wake up
return jobs.size() > 0 ||
stoken.stop_requested();
});
if (stoken.stop_requested()) { //Stop if requested to stop
break;
}
jobId = jobs.front(); //There is jobId. Grab it.
jobs.pop();
} //End of locked block
//Do job here outside the lock
std::cout << std::this_thread::get_id() << " "
<< "Doing job " << jobId << "\n";
} //End of while loop
}
void manager() {
std::stop_source ssource; //Create a stop source
//Create 2 workers and pass stop tokens
std::thread worker1(worker, ssource.get_token());
std::thread worker2(worker, ssource.get_token());
//Enqueue some jobs
for (int i = 0; i < 5; i++) {
{ //Locked block
std::unique_lock lck(mut);
jobs.push(i);
cv.notify_one(); //Wakes up only one worker
}
std::this_thread::sleep_for(1s);
}
//Stop all workers
ssource.request_stop();
//Join threads
worker1.join();
worker2.join();
}
std::stop_callback构造函数以原子方式注册回调。该回调在关联的std::stop_source上调用request_stop()的线程中执行,但如果在注册之前已请求停止,则会立即在注册线程中调用该回调。
事实上,在等待条件变量时,我们不必显式地使用std::stop_callback 。C++20 在std::condition_variable_any中引入了一个等待函数,它接受std::stop_token并在停止请求时唤醒等待线程。与std::condition_variable不同,std::condition_variable_any可以使用任何锁类型。实际上,它是std::condition_variable 的功能丰富的包装器,并使用std::stop_callback来唤醒线程。下面的代码显示了带有std::condition_variable_any 的上述工作代码:
//...
std::condition_variable_any cv_any;
void worker_cv_any(std::stop_token stoken) {
while (true) {
int jobId = -1;
{ //Locked block
std::unique_lock lck(mut);
//wait() function takes a stop token and predicate
// and returns 'predicate()' on signal
if(!cv_any.wait(lck, stoken, []() {
return jobs.size() > 0; //Condition to wake
})) {
/*Predicate returned false.
Therefore woke up because of a stop request. */
break; //Leave
}
//Grab jobId
jobId = jobs.front();
jobs.pop();
}
//Do job here outside the lock
std::cout << std::this_thread::get_id() << " "
<< "Doing job " << jobId << "\n";
} //End of while loop
}
std::stop_source、std::stop_token和std::stop_callback是引用共享停止状态的轻量级对象。默认构造std::stop_source时会创建一个新的停止状态。std::stop_source、关联的std::stop_token (s) 和std::stop_callback (s)的任何进一步副本都引用相同的停止状态。
停止状态记录停止事件并维护回调注册。只要至少有一个对象引用它,它就保持活动状态。下图显示了这些对象之间的典型关系:
std::jthread中的协作取消
std::jthread使用 C++20 协作取消模型。它创建并拥有一个std::stop_source。如果使用接受std::stop_token 的函数进行初始化,则 std::jthread从其停止源获取停止标记并将其传递给该函数。
std ::jthread的顶级函数不必采用停止标记,但长时间运行的线程应该始终能够满足取消请求。下面是std::jthread取消的示例:
void jthread_cancel() {
using namespace std::literals::chrono_literals;
//Lambda takes a stop token
std::jthread jt([](std::stop_token stoken) {
while (!stoken.stop_requested()) {
//Working...
std::this_thread::sleep_for(1s);
}
});
std::this_thread::sleep_for(5s);
//Thread is stopped and joined in jt.~jthread() here
}
这里有两点值得注意。一,如果传递的函数不采用std:: stop_token ,似乎没有指定std:: jthread构造函数是否初始化停止状态。第二,std::jthread无法使用外部std::stop_source进行初始化。在某些情况下,这些点可能很重要,因为它们会影响应用程序设计。
结论
std::jthread中的自动加入和协作取消使编写更安全的代码变得更加容易。
C++20 中的协作取消受到.NET 中用于取消托管线程的类似框架的启发。该框架不依赖于std::jthread ,甚至可以与std::thread或其他异步操作一起独立使用。