locked
C++方案中如何调试Windows商店应用程序中的任务(task)异常

    常规讨论

  • 快速响应,流利交互是开发Windows商店应用的主要原则之一,这就迫使异步编程在开发过程中无处不在。但是,当你使用C++开发Windows商店应用,并且使用异步编程方式时候,你可能会遇到一些没有被检测的任务异常,且这些是很难找到其发生原因。在这篇文章里,我想讨论如何调试Windows商店应用程序中的任务异常。

    这里,我用一个例子来阐述其调试的详细步骤。我创建一个简单的商店应用,通过异步API去选取一个文件并且显示其内容。具体的代码段如下:

    XAML代码:

    <StackPanel Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
      <Button x:Name="btnPicker" Content="Open File..." Click="Picker"/>
      <TextBlock x:Name="txtBlockOutput" Width="100"/>
      <Image x:Name="ImageFromFile" Stretch="Fill"/>
    </StackPanel>

    C++代码:

    void WindowsRTPicker::MainPage::Picker(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e)
    {
      FileOpenPicker^ openPicker = ref new FileOpenPicker();
      openPicker->ViewMode = PickerViewMode::Thumbnail;
      openPicker->SuggestedStartLocation = PickerLocationId::PicturesLibrary;
      openPicker->FileTypeFilter->Append(".jpg");
      create_task(openPicker->PickSingleFileAsync()) //get the file
      .then([this](StorageFile^ file) 
      { 
        txtBlockOutput->Text = "Picked photo: " + file->Name;
        create_task(file->OpenAsync(FileAccessMode::Read)). //open the file
        then([=](IRandomAccessStream^ bitmapStream)
        {
          create_task([this](){
          wait(1000); //emulate an expensive task;
        }).then([=]() 
          { 
            BitmapImage^ bitmapImage = ref new BitmapImage();
            bitmapImage->SetSource(bitmapStream);
            ImageFromFile->Source = bitmapImage;
          },task_continuation_context::use_current());
        });
      });
    }

    但是,这个代码有些问题,当我们在Visual Studio 2012中调试代码的时候,他将提示有一个未处理的异常:

    异常消息说,有一个无效的参数传递给了函数造成函数参数无效。这个消息意义不大。我们通过调用堆栈也找不任何线索:

    msvcr110d.dll!0faa9ad1()
    WindowsRTPicker.exe!Concurrency::details::_ExceptionHolder::~_ExceptionHolder() Line 882
    WindowsRTPicker.exe!Concurrency::details::_ExceptionHolder::`scalar deleting destructor'(unsigned int)
    WindowsRTPicker.exe!std::_Ref_count_obj<Concurrency::details::_ExceptionHolder>::_Destroy() Line 885
    WindowsRTPicker.exe!std::_Ref_count_base::_Decref() Line 120
    WindowsRTPicker.exe!std::_Ptr_base<Concurrency::details::_ExceptionHolder>::_Decref() Line 347
    WindowsRTPicker.exe!std::shared_ptr<Concurrency::details::_ExceptionHolder>::~shared_ptr<Concurrency::details::_ExceptionHolder>() Line 624
    WindowsRTPicker.exe!Concurrency::details::_Task_impl_base::~_Task_impl_base() Line 1294
    WindowsRTPicker.exe!Concurrency::details::_Task_impl<unsigned char>::~_Task_impl<unsigned char>() Line 1972
    WindowsRTPicker.exe!Concurrency::details::_Task_impl<unsigned char>::`scalar deleting destructor'(unsigned int)
    WindowsRTPicker.exe!std::_Ref_count_obj<Concurrency::details::_Task_impl<unsigned char> >::_Destroy() Line 884
    WindowsRTPicker.exe!std::_Ref_count_base::_Decref() Line 120
    WindowsRTPicker.exe!std::_Ptr_base<Concurrency::details::_Task_impl<unsigned char> >::_Decref() Line 347
    WindowsRTPicker.exe!std::shared_ptr<Concurrency::details::_Task_impl<unsigned char> >::~shared_ptr<Concurrency::details::_Task_impl<unsigned char> >() Line 624
    WindowsRTPicker.exe!Concurrency::details::_PPLTaskHandle<unsigned char,Concurrency::task<unsigned char>::_ContinuationTaskHandle<void,void,<lambda_af0a206e09a66eb8e507263ce4c9dbe9>,std::integral_constant<bool,0>,Concurrency::details::_TypeSelectorNoAsync>,Concurrency::details::_ContinuationTaskHandleBase>::~_PPLTaskHandle<unsigned char,Concurrency::task<unsigned char>::_ContinuationTaskHandle<void,void,<lambda_af0a206e09a66eb8e507263ce4c9dbe9>,std::integral_constant<bool,0>,Concurrency::details::_TypeSelectorNoAsync>,Concurrency::details::_ContinuationTaskHandleBase>() Line 1200
    WindowsRTPicker.exe!Concurrency::task<unsigned char>::_ContinuationTaskHandle<void,void,<lambda_af0a206e09a66eb8e507263ce4c9dbe9>,std::integral_constant<bool,0>,Concurrency::details::_TypeSelectorNoAsync>::~_ContinuationTaskHandle<void,void,<lambda_af0a206e09a66eb8e507263ce4c9dbe9>,std::integral_constant<bool,0>,Concurrency::details::_TypeSelectorNoAsync>() Line 3313
    WindowsRTPicker.exe!Concurrency::task<unsigned char>::_ContinuationTaskHandle<void,void,<lambda_af0a206e09a66eb8e507263ce4c9dbe9>,std::integral_constant<bool,0>,Concurrency::details::_TypeSelectorNoAsync>::`scalar deleting destructor'(unsigned int)
    msvcr110d.dll!0fa730bc()
    msvcr110d.dll!0faa9721()
    msvcr110d.dll!0fa73afa()
    msvcr110d.dll!0fa83063()
    msvcr110d.dll!0faa4bc1()
    msvcr110d.dll!0fa7fd07()
    msvcr110d.dll!0fa7fbd3()
    msvcr110d.dll!0fa7e65c()
    msvcr110d.dll!0faabea2()
    kernel32.dll!76c98543()
    ntdll.dll!775dac69()
    ntdll.dll!775dac3c()
    

    在堆栈中没有涉及到任何的用户代码。然而,我们可以发现一个有趣的对象:
    WindowsRTPicker.exe!Concurrency::details::_ExceptionHolder::~_ExceptionHolder() Line 882
    _ExceptionHolder是原始异常信息的包装对象。让我们看看这个_ExceptionHolder对象中包含哪些内容:

    M_winRTException 和 _M_disassembleMe 是_ExceptionHolder的两个重要成员字段。
    如果你的错误是一个平台异常,则M_winRTException为非空。你可以展开它,从__hresult 和 __throwInfo 字段来找到错误代码和异常信息。__hresult 是 0x800100e , __throwInfo 显示是 WrongThread (错误线程)异常。

    现在我们知道了真正的错误信息。但是我们仍然想要知道是哪条线程发生了这个问题。这样让我们来检查下_M_disassembleMe 字段的值。复制这个字段的值(这里是0x00d78207),将其粘贴到反汇编窗口,然后按Enter:

    这样,他会移动指示到我们的代码调用了task::then函数的指令。

    在此处右键单击,选择转到源代码

    然后你就可以看到引发这个异常的实际代码了。

    我们知道,所有控件必须在UI线程才能更新UI内容和属性。然而,这里的异步代码导致了在不同于UI线程的线程中更新修改了控件内容,引发了WrongThread异常。

    所以,解决方法是要通过调度器将代码调度回UI线程执行,如下:

    auto dispatcher=CoreWindow::GetForCurrentThread()->Dispatcher;
    create_task(file->OpenAsync(FileAccessMode::Read)). //open the file
    then([=](IRandomAccessStream^ bitmapStream)
    {
        create_task([this]()
        { 
           wait(1000); //模拟耗时操作
        })
        .then([=]()
        {
          dispatcher->RunAsync(CoreDispatcherPriority::Normal,
          ref new Windows::UI::Core::DispatchedHandler([=]()
          {
              BitmapImage^  bitmapImage = ref new BitmapImage();
              bitmapImage->SetSource(bitmapStream);
              ImageFromFile->Source = bitmapImage;
          }));
        });
    });
    

    这样代码可以正常工作了,但是还不够谨慎,你可能还会有一个问题:在第一个then中还会更新UI控件:

    .then([this](StorageFile^ file)
    {
      txtBlockOutput->Text = "Picked photo: " + file->Name;
      create_task(file->OpenAsync(FileAccessMode::Read)). //open the file
      then([=](IRandomAccessStream^ bitmapStream)
      {
        create_task([this](){
    wait(1000); //模拟耗时操作
    

    但它为什么不会引发 WrongThread 异常?原因是第一个任务会返回异步操作到UI线程,但第三个不会。
    通常,你不需要假象和设定代码会在某个线程上运行。但是Windows商店应用是个例外,他仅允许UI线程来控制UI组件,如果一个延续链(.then操作)是在UI线程被创建的,那么一个有返回的异步操作会将他的延续链回到UI线程上执行;如果任务不会返回异步操作,则他的延续链就无法预知了。默认情况下,他的延续链会在第一个可用的后台线程上运行。

    在这种情况下,第一个任务在UI线程上创建WinRT的异步操作,,并且具有返回,因此他被安排回 UI线程。第三个任务不会返回操作,因此其延续链不会被安排到UI线程,则导致WrongThread异常。

    总之,这篇文章阐释了如何排查任务异常问题,我们可以从_ExceptionHolder 对象获取真实的异常信息。并且,我们还讨论一个典型的异步问题。


    Bob Bao [MSFT]
    MSDN Community Support | Feedback to us

    2012年11月7日 9:47
    版主