曾经在《【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来终止线程时的安全与便捷,只要把以上提到的一些关键概念搞清楚,相信开头所述的那些问题应该都能控制和避免。