【Windows8开发】异步编程进阶篇之 单线程套间(STA)及如何控制task执行上下文

(请大家注意了,本文涉及的概念相当重要,开发中相当管用)
开始就先来看一段代码:
void SampleCpp::MainPage::Btn_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
{
     auto workItemDelegate = [this]() {
          // do something here
     };
     create_task(workItemDelegate).then([this]{
          this->Btn->Content = "New";
     });
}

本意是在执行完workItemDelegate中的一些处理后更新UI中某个按钮名,却发现抛出异常了,按钮名没有被更新。然后如下稍微修改一下,再看看结果:
void SampleCpp::MainPage::Btn_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
{
     auto op = create_async([this]() {
          // do something here
     });
     create_task(op).then([this]() {
         this->Btn->Content = "New";
     });
}

这回一切正常了。为什么呢?让我们带着这个问题进入本文吧。

先来理解一个概念:STA(single-threaded apartment),暂且翻译为单线程套间,熟悉Com线程模型的可能知道这个概念。由于不太容易解释它,这里就尽我所能简单的描述说明下。比如模块中有一组功能,不管外界如何调用这些功能,也不管外界在多少个线程中调用了这些功能,需要保证这些功能永远都只在同一个线程中运行。STA模块会管理一个消息队列,收到一个消息它就会执行消息中指定功能的调用,然后把执行结果返回给调用方。而外部不同线程中调用这些功能其实就是给该模块发送一系列调用的消息,在模块消息队列中管理执行,因此可以保证它们都在同一线程中执行。不理解的可以google下single-threaded apartment这个概念,详解的文章应该很多。
之所以会提到STA,因为在Metro程序中UI就是运行在一个单线程套间中(STA),所有涉及UI更新的操作都必须在UI线程中执行。之前第一段代码之所以会出异常,就是因为then中对UI的更新处理会在background线程中执行,违背了UI单线程套间的原则。那第二段又为什么会结果正常呢?
原因是WinRT有这样的定义:当task的Lambda函数返回类型为IAsyncAction或者IAsyncOperation时,此task如果在单线程套间中被创建,则默认情况下,其所有的后续任务(then中的处理)仍旧会在此单线程套间中被执行。如上第二段代码中create_async返回值为IAsyncAction,因此then中更新UI的处理其实仍旧运行在UI线程中,能正常被执行。甚至后续即使连续有多个then处理,也是一样,所有的task continuation都在该task的单线程套间(该例中其实就是UI线程套间)中执行,所以如下的代码都能如你所想,跑的转。
void SampleCpp::MainPage::Btn_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
{
     auto op = create_async([this]() {
          // do something here
     });
     create_task(op).then([this]() {
         this->Btn->Content = "New1";
     }).then([this]() {
         this->Btn->Content = "New2";
     });
}

到这里肯定会有人问,除了通过task的Lambda返回值来决定task执行上下文,代码中是否可以自己来决定呢?答案当然是肯定的。WinRT提供了task_continuation_context,我们可以使用其中如下三个静态方法:
task_continuation_context::use_current()     在当前上下文中执行task
task_continuation_context::use_arbitrary()   在background线程中执行task
task_continuation_context::use_default()     在默认情况下执行task
我们可以把如上这三个方法的返回值作为then函数的第二个参数传递(函数默认参数就等同于传use_default),看下代码一切就清楚了,我们把开始第一段出错的代码如下改一下,通过use_current使then函数中的处理也运行于UI线程中,执行结果就正常了:
void SampleCpp::MainPage::Btn_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
{
     auto workItemDelegate = [this]() {
          // do something here
     };
     auto context = task_continuation_context::use_current();
     create_task(workItemDelegate).then([this]{
          this->Btn->Content = "New";
     }, context);
}

那如何决定使用哪种执行上下文呢?这个相信不用我说就明白了吧。一般情况下,逻辑算法的执行都可以使用use_arbitrary让其在background线程中执行,这样不会影响到UI的响应。而跟UI有关的处理则需要通过use_current指定到UI线程中执行。

如果说到这里,你都没有觉得这种处理方式有何优越之处,或者说不觉得有多大用处,那或许在多线程开发,线程模型这些方面你还略显稚嫩,要知道一般要建立概括到UI的线程模型时,不可避免需要考虑当后台线程起来执行后如何再抛回UI线程的问题,要知道如果想用传统的thread API想要实现类似特性又兼顾API的易用性和稳定性的话,不是一个容易的活。所以当发现使用winRT的task能很方便的实现了一些功能时,我是一喜一愁,喜的是使用WinRT的task来进行并行任务开发实在是太方便,效率太高了,MS真不是盖的,而愁的是,WinRT对并行开发,异步编程,多线程开发来了个API大变脸,这为一些需要跨平台的模块的开发增加了很多难度。

最后再额外提一点,如开头代码中的问题,在使用WinRT的ThreadPool时也会发生类似问题,如下程序也不能正常更新UI:
auto workItemDelegate = [this](IAsyncAction^ workItem) {
       this->Btn->Content = "thread";
};
auto workItemHandler = ref new Windows::System::Threading::WorkItemHandler(workItemDelegate);
Windows::System::Threading::ThreadPool::RunAsync(workItemHandler, Windows::System::Threading::WorkItemPriority::Low);

虽然我们可以通过Windows::UI::Core::CoreDispatcher中的Dispatcher来解决这个问题,但是相较于task的解决方法,麻烦了很多,这也是为什么我在异步编程进阶篇系列开篇文章中说尽可能不用WinRT的ThreadPool,因为task更强大,更方便。
后续还会有task的一些特性介绍给大家,有不同观点的欢迎留言指正!

你可能感兴趣的:(多线程,编程,windows,UI,object,System)