本文参考了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
中查看相应的实现。
参考
- await 2.0 - Stackless Resumable Functions - Gor Nishanov - CppCon 2014.pdf
- Putting Coroutines to Work with the Windows Runtime Slides
- Embracing Standard C++ for the Windows Runtime Slides
- CppCon 2017: Toby Allsopp “Coroutines: what can’t they do?”
(完)