先考虑下使用传统Thread API时,线程间的异常控制往往是一件相当痛苦的事情,由于某个线程的异常未捕获导致整个程序crash的情况也许很多人都碰到过。比如如果有一组任务链处理序列,我们往往需要对每个线程都详细考虑其可能抛出的各种异常,如果需要把一个异常从一个线程抛到另外一个线程进行处理时,那更需要多费很多周折,而且往往费时费力最后却还总是会发现这样那样的疏忽与漏洞。那WinRT提供的task有何不同呢?
在给出答案之前,先介绍下Metro程序中如何使用WinRT中定义Exception。WinRT中所有的异常都继承于Platform::Exception,比如ClassNotRegisteredException,FailureException,COMException等等。例如要抛出一个Failure的异常:
throw ref new Platform::Exception(E_FAIL); // or
throw ref new Platform::FailException();
但是,WinRT中不允许从Platform::Exception继承来定义第三方的Exception类,所以我们只能使用winerror.h中已定义的异常类型(如E_FAIL)。那如果需要自定义怎么办?可以使用Platform::COMException,COMException可以接受第三方通过MAKE_HRESULT自定义的异常。
throw ref new Platform::COMException(E_MY_EXCEPTION);
(注:C++原生的异常机制还是支持的,模块内部可以照常使用,但面向UI或者外部模块的public接口并不推荐使用,还是推荐使用WinRT提供的异常类型)
简单介绍完WinRT中的异常后,让我们回到task,考虑下在task中是如何控制异常的。
前面的文章中曾经介绍过,task continuation中value-based和task-based的区别。当你希望前任务发生异常或某些情况下被cancel时,后序任务仍旧能够正常执行,那就应该使用task-based的形式,而反之,则使用value-based,后序的任务将不会被执行。看下面这个例子:
create_task([] {
throw ref new Platform::Exception(E_FAIL);
}).then([] ()
{
throw ref new Platform::Exception(E_FAIL);
}).then([] (task<void> t)
{
try {
t.get();
} catch (Platform::Exception^ e) {
// handle the exception
}
});
执行结果是这样的,初始任务中E_FAIL异常被抛出,第一个then中的任务处理未被执行,第二个then中的任务捕获了开始抛出的E_FAIL异常。
简单解释一下,当开始的task抛出异常时,由于第一个then中的处理是value-based,所以不再会被执行,而第二个then是task-based的(参数为task<void>),所以仍旧会被调用,其中t.get()处理的一个作用是可以获得之前task的返回值,虽然本例中并无返回值,但get还有个作用是可以抛出前期任务中发生的所有异常。也就是说,我们可以在task的最后增加一个延续任务(Continuation),在此任务中通过get方法抛出所有前期task发生的异常,并在此处统一catch处理。这可以让我们很方便地控制前后task中发生的所有异常。
当然,正常的处理并不是去捕获所有发生的异常( 不论是预期内还是预期外)来保证程序不会crash,而是应该去处理在该处应该处理的异常,而忽略一些期望外的异常,让这些异常继续被抛出,即使导致程序crash,也不至于埋藏掉一些本该很容易被发现的错误。
到此,不知大家是否也有相同的感触,task的这种异常处理的方式,可以帮助开发者很方便的解决一些使用传统thread API很难解决的异常控制问题。