std::jthread 您常用么?看完这篇就可以实战

随着时代的发展,你所碰到过的代码问题,不钻牛角尖的扯前沿算法之类,可以说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_tokenstd::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_sourcestd::stop_tokenstd::stop_callback是引用共享停止状态的轻量级对象。默认构造std::stop_source时会创建一个新的停止状态。std::stop_source、关联的std::stop_token (s) 和std::stop_callback (s)的任何进一步副本都引用相同的停止状态。

停止状态记录停止事件并维护回调注册。只要至少有一个对象引用它,它就保持活动状态。下图显示了这些对象之间的典型关系:

C++20 中的协作取消


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或其他异步操作一起独立使用。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值