前些天有人贴出了一些代码来让 Async/Await 用起来更简单。他们走的方向是错的,看起来更像是 OnNext/OnError/OnCompleted 的 RXs 模型。错误的原因在于,它并不支持等待。
通常来说,你写的任何 Asyn 代码都会在某个时刻需要同步,所以它必须是可等待的。这就是为什么 async void 是个非常非常狡猾的用法,只应在 event handler 使用,但我们先别提那些了。
我跟写原先那篇文章的小伙子谈话时,指出更典型的用法应该是这样的:
class Program { static void Main(string[] args) { new Program().Backup(); Console.ReadLine(); } public async void Backup() { var backupStatus = await BackupAsync(); } public async TaskBackupAsync() { return await Task.Run(() => "groovy"); } }
另一个犀利的读者指出,你永远不应该用 async void (我这样写是为了尽量跟原作者贴的代码保持一致),而我的代码能正常运行的唯一原因是用了 Console.ReadLine(),阻塞了主线程。
我确实知道我永远不该用 async void ,所以我开始尝试贴出一个改进版本。重要的一点是,我用的是 ConsoleApplication 。所以我尝试这样:
class Program { private static async void Main(string[] args) { await new Program().Backup(); } public async Task Backup() { var backupStatus = await BackupAsync(); await Task.Delay(5000); //simulate some work Console.WriteLine(backupStatus); } public async TaskBackupAsync() { return await Task.Run(() => "groovy"); } }
编译器发出了抱怨,不允许 async void 的 main 方法。Error 如下:
‘NonAwaitingConsoleApplication.Program.Main(string[])’: an entry point cannot be marked with the ‘async’ modifier
好吧,那改成这样呢?
class Program { private static void Main(string[] args) { Task.Run(() => MainAsync(args)).Wait(); } static async void MainAsync(string[] args) { Console.WriteLine(DateTime.Now.ToLongTimeString()); await new Program().Backup(); Console.WriteLine(DateTime.Now.ToLongTimeString()); } public async Task Backup() { var backupStatus = await BackupAsync(); await Task.Delay(5000); //simulate some work Console.WriteLine(backupStatus); } public async TaskBackupAsync() { return await Task.Run(() => "groovy"); } }
结果它直接闪退了……嗯。有意思。原因在于,在 ConsoleApplication 中没有 SynchronizationContext 。所以线程直接返回到了操作系统,导致程序提前退出。并不是我们想要的结果。怎么办呢?
幸运的是,有一个非常聪明的小伙子 (我高度赞扬) 叫 Stephen Cleary ,他写了一个很棒的 Extension 集,共同参与开发的还有 Stephen Toub (他显然是经验丰富的),所以我充分信任这个库。它叫 NitoAsyncEx 。也可以从 NuGet 获取到它: Nito.AsyncEx
总之,有了这个库,我们就可以写出这样的 AwaitingConsoleApplication 了:
internal class Program { private static void Main(string[] args) { AsyncContext.Run(() => MainAsync(args)); } static async void MainAsync(string[] args) { Console.WriteLine(DateTime.Now.ToLongTimeString()); await new Program().Backup(); Console.WriteLine(DateTime.Now.ToLongTimeString()); } public async Task Backup() { var backupStatus = await BackupAsync(); await Task.Delay(5000); //simulate some work Console.WriteLine(backupStatus); } public async TaskBackupAsync() { return await Task.Run(() => "groovy"); } }
注意其中用到的 AsyncContext ,是神奇的 NitoAsyncEx 类让它成为可能。我强烈建议你看看这个类的内部实现 (可以用 Reflector 看到,或者看 Codeplex 上的 source ),看看它的原理。 Stephen 替你做了很多工作,例如确保存在有效的 SynchronizationContext 。请有空看一看。最好看看整个库,它是个很有用的库。
运行这段代码,输出如下,正是我们想要的;然后就退出了(正常退出)
10:52:21
groovy
10:52:26
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务