本文参考了Putting Coroutines to Work with the Windows Runtime Slides,来了解一下C++/WinRT是如何使用Coroutine的。

回调、还是回调

如果使用C++/CX来编写WinRT的异步代码,需要使用许多回调函数,如下所示:

task<String ^> OcrAsync(String ^ path)
{
  return create_task(StorageFile::GetFileFromPathAsync(path))
    .then([](StorageFile ^ file) {
          return file->OpenAsync(FileAccessMode::Read);
          })
  .then([](IRandomAccessStream ^ stream) {
        return BitmapDecoder::CreateAsync(stream);
        })
  .then([](BitmapDecoder ^ decoder) {
        return decoder->GetSoftwareBitmapAsync();
        })
  .then([](SoftwareBitmap ^ bitmap) {
        OcrEngine ^ engine = OcrEngine::TryCreateFromUserProfileLanguages();
        return engine->RecognizeAsync(bitmap);
        })
  .then([](OcrResult ^ result) {
        return result->Text;
        });
}

上面的例子有点像糖葫芦串。使用task来进行异步操作,task的then()函数来在task执行完毕的时候调用回调函数。而每个回调函数又返回一个新的task,可以串联新的then()函数来执行新的回调,返回新的task。上述形式可以看成是一个continuation。每个以Async函数异步执行函数内容(大概是从线程池选择一个空闲的线程来执行这个任务)。

你大概会猜到,上面的方式并不一定是最优的。一个异步任务结束时,每个回调函数又生成一个新的异步任务(从而又需要从线程池找一个空闲的线程来执行此操作)。为什么不直接让一个空闲的线程执行全部的任务?而不是每次创建任务的时候重新找。

下面使用Coroutine的等效代码:

std::future<hstring> AsyncSample(hstring_ref path)
{
  StorageFile file = co_await StorageFile::GetFileFromPathAsync(path);
  IRandomAccessStream stream = co_await file.OpenAsync(FileAccessMode::Read);
  BitmapDecoder decoder = co_await BitmapDecoder::CreateAsync(stream);
  SoftwareBitmap bitmap = co_await decoder.GetSoftwareBitmapAsync();
  OcrEngine engine = OcrEngine::TryCreateFromUserProfileLanguages();
  OcrResult result = co_await engine.RecognizeAsync(bitmap);
  co_return result.Text();
}

可以看到代码清晰了一些,但其实内涵是差不多的,都是continuation,背后都是一串一串回调。比如在执行co_await StorageFile::GetFileFromPathAsync(path)之后,Coroutine AsyncSample挂起,然后会在co_await的await_suspend()中插入一个回调,用于恢复Corutine剩余部分的执行,这其实相当于:

  return create_task(StorageFile::GetFileFromPathAsync(path)) // 挂起Coroutine
    .then([](StorageFile ^ file) {
          // 恢复Coroutine
          return create_task( file->OpenAsync(FileAccessMode::Read)) // 挂起Coroutine
          .then([](IRandomAccessStream ^ stream) {
            // 恢复Coroutine
            return create_task...;
          })
 })

因为C++的Coroutine目前是Stackless的,没有自己的堆栈,所以将恢复点放置在回调函数中,也实属无奈,只有栈顶层的Coroutine可以被挂起,恢复也只能恢复在栈顶。所以被挂起的Coroutine只能以某种链状结构存储起来。

另一个例子

请看里面的注释:

IAsyncAction ForegroundAsync(TextBlock block)
{
  FileOpenPicker picker = ...
    auto file = co_await picker.PickSingleFileAsync(); 
  // 1. 假设当前在UI线程,上面的co_await会导致ForegroundAsync会被挂起,并且会在PickSingleFileAsync完成时恢复,但那时候又会恢复在UI线程中执行
  block.Text(co_await BackgroundAsync(file));
// 3. 上面的co_await执行时,BackgroundAsync已经在另外一个线程了。不过co_await会将ForegroundAsync挂起,并设置在BackgroundAsync完成时的回调函数中在UI线程恢复执行,并设置block.Text的值。
}

IAsyncOperation<hstring> BackgroundAsync(StorageFile file)
{
  co_await resume_background();
  // 2. 上面的co_await会将BackgroundAsync挂起,并在另外一个线程恢复,相当于切换了一个线程执行
  auto stream = co_await file.OpenAsync(FileAccessMode::Read);
  ...
    auto result = co_await engine.RecognizeAsync(bitmap);
  return result.Text();
}

简单地说,在IAsync*类型上使用co_await最终会回到调用线程,而在resume_background上使用则不会。

C++/WinRT一共定义了四种IAsync*类型:

  • IAsyncAction
  • IAsyncActionWithProgress
  • IAsyncOperation
  • IAsyncOperationWithProgress<T, P>

上面每一中,既可以作为coroutine的返回类型,同时也是awaitable类型。他们是通过operator co_await把自己变为awaitable的。operator co_await返回一个await_adapter,其await_suspend()方法会注册一个操作结束时的回调,在这个回调函数中在原来的context恢复自己的执行。

WindowsSDK中带有cppwinrt的实现。比如可以在C:\Program Files (x86)\Windows Kits\10\Include\10.0.17763.0\cppwinrt\winrt\base.h中查看相应的实现。

参考

(完)