【Windows8开发】异步编程进阶篇之 谈task如何避免线程取消引发的系列问题

时间:2023-01-10 19:01:22
曾经在《【Windows8开发】异步编程进阶篇之 对thread说不,用task 一文中说明过在Metro程序中推荐使用WinRT的task,而避免使用传统的线程API,理由之一就是当使用传统线程调用方式,在人为(通过代码)去终止一个线程时经常会引发一些意料之外的异常情况,导致程序异常或崩溃,让我们回忆一下任务(thread)取消引发的系列问题:
1. 强制终止一个线程时,该线程占用资源未释放,导致异常
2. 强制终止A线程,并通过消息传递希望连带终止所有与A相关后续线程时,没有达成目的(遗漏若干),导致异常
3. 线程组间逻辑关系复杂,终止任一线程时都要引发其他线程处理,绞尽脑汁仍旧除了纰漏,Bug不断
4. 通常需要框架层提供封装良好的线程(组)取消机制,当设计人员能力不足,设计不严密时,Bug会在上层遍地开花。
5. .....

基于任务(thread)取消引发的这一系列问题,让我们看看WinRT提供的task可以带来哪些改善。先看个例子:
cancellation_token_source cts;
auto token = cts.get_token();
auto t = create_task([]
{
       bool isCanceled = false;
       while (isCanceled == false)
       {
              if (is_task_cancellation_requested())
              {
                     cancel_current_task();
              }
       }
}, token);
wait(1000);
cts.cancel();
// Wait for the task to cancel.
t.wait();
(注:wait(1000)是为了保证在被取消前task能被正常创建,否则在调用cancel时该task还未正常初始化执行。如果在Metro程序中,以上代码由于存在1s的延时,不能直接运行在UI线程中,否则会阻塞UI线程导致异常,请包在create_task中来执行)

初看处理逻辑其实跟传统很多线程库的封装有点类似。简单解释一下,创建一个cancellation_token_source类型的用于取消任务的标记,获得该标记的token传递给create_task创建的task,当调用cancellation_token_source的cancel方法后,任务可以通过is_task_cancellation_requested和cancel_current_task来判断和终止一个task。把代码继续完善一下:
cancellation_token_source cts;
auto token = cts.get_token();
auto t = create_task([]
{
       bool isCanceled = false;
       while (isCanceled == false)
       {
              if (is_task_cancellation_requested())
              {
                     cancel_current_task();
              }
       }
}, token).then([](task<void> t){
       try {
              t.get();
       } catch (task_canceled& tt) {
              // handle the task cancel
       }
});
wait(1000);
cts.cancel();
// Wait for the task to cancel.
t.wait();

由于在task中调用cancel_current_task,会抛出task_canceled异常,在then中task-based类型任务的延续处理中,通过task的get方法可以捕获该异常。关于task-based与value-based任务的区别,以及task异常处理请参考如下文章:

从上面的示例可以看出,结合task-based和value-based以及task的get方法,可以很方便的控制一个序列任务的执行,比如A,B两个task,A执行完希望执行B,如果希望A任务被取消时B任务也不执行,则可以把B的处理设置为value-based,相反则使用task-based,如果希望在task被取消时获得通知,则可以如上通过get方法来捕获cancel异常来进行处理,而WinRT还提供另一种通过concurrency::cancellation_token::register_callback注册回调的方式来响应task的取消操作:
cancellation_token_source cts;
auto token = cts.get_token();
cancellation_token_registration cookie;
cookie = token.register_callback([token, &cookie]()
{
     token.deregister_callback(cookie);
     // do something in callback
});
auto t = create_task([]
{
       while(true) {
              if (is_task_cancellation_requested())
                     cancel_current_task();
       }
}, token);

wait(1000);
cts.cancel();
t.wait();

通过register_callback把回调方法注册给token,然后把该token传递给task,当task被取消时会触发此回调。
以上示例中都是在起始task中调用is_task_cancellation_requested来判断task是否被取消,那在后续的一系列then的延长task中呢?看如下两段代码:
第一段:
cancellation_token_source cts;
auto token = cts.get_token();
auto t = create_task([]
{
       // do something here
}, token).then([](){
       while(true) {
              if (is_task_cancellation_requested())
                     cancel_current_task();
       }
});
wait(1000);
cts.cancel();
t.wait();

第二段:
cancellation_token_source cts;
auto token = cts.get_token();
auto t = create_task([]
{
       // do something here
}, token).then([](task<void> t){
       while(true) {
              if (is_task_cancellation_requested())
                     cancel_current_task();
       }
});
wait(1000);
cts.cancel();
t.wait();

执行后会发现,第一段能正常工作,而第二段task不会终止,cancel的token在第二段中无效,为什么呢?
这里涉及一个概念,当后续task为value-based时,它会继续沿用起始任务中的token,当为task-based时,则不会,只能显式的把token传递给task-based型延续任务,处理代码如下:
cancellation_token_source cts;
auto token = cts.get_token();
auto t = create_task([]
{
       // do something here
}, token).then([token](task<void> t){
       while(true) {
               if (token.is_canceled())
                    cancel_current_task();
       }
});
wait(1000);
cts.cancel();
t.wait();

顺便再说明一个概念,当使用create_async创建task时,task的取消方法略有不同,create_async返回IAsyncAction等类型,可以直接调用其Cancel方法来终止task,而不需要显示的去创建cancellation_token_source。
IAsyncAction^ op = create_async([](cancellation_token token)
{
       return create_task([token]{
              while(1) {
                     if (token.is_canceled())
                           cancel_current_task();
              }
       });
});
op->Cancel();

但是需要注意的是当在create_async中又通过create_task创建了内部task后,如果希望在调用Cancel函数时能触发内部task也终止,则需要显式的传递cancellation_token给内部task,代码如下:
IAsyncAction^ op = create_async([](cancellation_token token)
{
       return create_task([token]{
              while(1) {
                     if (token.is_canceled())
                           cancel_current_task();
              }
       });
});
op->Cancel();

最后,再简单看下task group中如何取消任务,处理很简单,只show代码,不解释了。
structured_task_group tg;
auto t1 = make_task([&] {
     tg.cancel();
});
auto t2 = make_task([&] {
     while(true) {
          if (tg.is_canceling()) {
          	break;
    	  }
     }
});
tg.run(t1);
tg.run(t2);
tg.wait();
task group相关细节请参考 【Windows8开发】异步编程进阶篇之 task group的几种方式及其间的区别 》。
总结一下,如果你理解了task异常控制以及task cancellation常用的API,你肯定能体会到通过WinRT的task来终止线程时的安全与便捷,只要把以上提到的一些关键概念搞清楚,相信开头所述的那些问题应该都能控制和避免。