将 async/await 异步代码转换为安全的不会死锁的同步代码
在 async/await 异步模型(即 TAP Task-based Asynchronous Pattern)出现以前,有大量的同步代码存在于代码库中,以至于这些代码全部迁移到 async/await 可能有些困难。这里就免不了将一部分异步代码修改为同步代码。然而传统的迁移方式存在或多或少的问题。本文将总结这些传统方法的坑,并推出一款异步转同步的新方法,解决传统方法的这些坑。
背景问题和传统方法
- 为什么有些方法不容易迁移到
async/await?- 参见微软的博客
async/await最佳实践 Async/Await - Best Practices in Asynchronous Programming。如果某个方法从同步方法修改为异步方法(例如从var content = file.Read()修改为var content = await file.ReadAsync()),那么调用此方法的整个调用链全部都要改成async/await才能让返回值在调用链中成功传递。
- 参见微软的博客
- 传统的异步转同步的方法有哪些?有什么坑?
- 参见我的好朋友林德熙的博客 win10 uwp 异步转同步。文章里使用
Task.Wait()或者Task.Result来获取异步方法的返回值。 - 这种方法会阻塞调用线程。如果调用线程是 UI 线程,那么 UI 将会无响应;更严重地,如果 UI 线程使用
DispatcherSynchronizationContext(参见我的另一篇文章 DispatcherSynchronizationContext - walterlv)进行线程上下文的同步,那么极有可能会造成死锁(参见我的另一篇文章 使用 Task.Wait()?立刻死锁(deadlock))。
- 参见我的好朋友林德熙的博客 win10 uwp 异步转同步。文章里使用
安全的方法
传统方法的坑在于 UI 线程无响应和死锁问题。既要解决无响应问题,又要阻塞调用方,可选的方法就是 Windows 消息循环了。在使用消息循环时还要避免使用 async/await 的同步上下文(SynchronizationContext),这样才能避免 UI 线程的死锁问题。
所以,我考虑使用 PushFrame 来阻塞当前线程并创建一个新的消息循环。使用 Task.ContinueWith 来恢复阻塞,而不使用 Task 中默认同步所采用的同步上下文。
代码如下:
/// <summary>
/// 通过 PushFrame(进入一个新的消息循环)的方式来同步等待一个必须使用 await 才能等待的异步操作。
/// 由于使用了消息循环,所以并不会阻塞 UI 线程。<para/>
/// 此方法适用于将一个 async/await 模式的异步代码转换为同步代码。<para/>
/// </summary>
/// <remarks>
/// 此方法适用于任何线程,包括 UI 线程、非 UI 线程、STA 线程、MTA 线程。
/// </remarks>
/// <typeparam name="TResult">
/// 异步方法返回值的类型。
/// 我们认为只有包含返回值的方法才会出现无法从异步转为同步的问题,所以必须要求异步方法返回一个值。
/// </typeparam>
/// <param name="task">异步的带有返回值的任务。</param>
/// <returns>异步方法在同步返回过程中的返回值。</returns>
public static TResult AwaitByPushFrame<TResult>(Task<TResult> task)
{
if (task == null) throw new ArgumentNullException(nameof(task));
Contract.EndContractBlock();
var frame = new DispatcherFrame();
task.ContinueWith(t =>
{
frame.Continue = false;
});
Dispatcher.PushFrame(frame);
return task.Result;
}
▲ 这就是全部代码了,仅适用于 Windows 平台(如果使用 .NET Core,需要额外的 Windows 兼容 NuGet 包)
新方法的适用范围和优劣
事实上,虽然我们使用了消息循环,但其实也适用于控制台程序,适用于各种各样奇奇怪怪的线程 —— 无论是 UI 线程还是非 UI 线程,无论是 STA 还是 MTA。
例如,我们现在在一个 MTA 线程模型的控制台程序中试用一下:
namespace Walterlv.Demo
{
class Program
{
static void Main(string[] args)
{
Console.Title = "walterlv's demo";
var foo = Foo();
var result = AwaitByPushFrame(foo);
Console.WriteLine($"输入的字符串为:{result}");
Console.ReadKey();
}
private static async Task<string> Foo()
{
Console.WriteLine("请稍后……");
await Task.Delay(1000);
Console.Write("请输入:");
var line = Console.ReadLine();
Console.WriteLine("正在处理……");
await Task.Run(() =>
{
// 模拟耗时的操作。
Thread.Sleep(1000);
});
return line;
}
}
}
启动控制台程序,我们发现程序真的停下来等待我们输入了。这说明一开始的 await Task.Delay(1000) 已经生效,Main 函数也没有退出。
▲ 开始运行
现在我们输入一段文字:
▲ 输入文字
依然正常。现在我们按下回车看看后台线程的执行是否也正常:
▲ 后台线程正在处理
后台线程也在处理,而且现在才停到 Main 函数的 ReadKey 中。说明转同步过程成功。
不过我们也要认识到,由于使用了消息循环,这意味着此方法不像 Task.Wait() 或 Task.Result 方法那样在全平台通用。不过,消息循环方法的出现便主要是用来解决 UI 的无响应和死锁问题。
总结
我们使用消息循环的方式完成了异步方法转同步方法,这样的方式不止能解决传统 Task.Wait()/Task.Result 导致 UI 线程无响应或死锁问题之外,也适用于非 UI 线程,不止能在 STA 线程使用,也能在 MTA 线程使用。
将 async/await 异步代码转换为安全的不会死锁的同步代码的更多相关文章
- 【转】C# Async/Await 异步编程中的最佳做法
Async/Await 异步编程中的最佳做法 Stephen Cleary 近日来,涌现了许多关于 Microsoft .NET Framework 4.5 中新增了对 async 和 await 支 ...
- 深入理解协程(四):async/await异步爬虫实战
本文目录: 同步方式爬取博客标题 async/await异步爬取博客标题 本片为深入理解协程系列文章的补充. 你将会在从本文中了解到:async/await如何运用的实际的爬虫中. 案例 从CSDN上 ...
- .NET Web应用中为什么要使用async/await异步编程
前言 什么是async/await? await和async是.NET Framework4.5框架.C#5.0语法里面出现的技术,目的是用于简化异步编程模型. async和await的关系? asy ...
- C#中 Thread,Task,Async/Await 异步编程
什么是异步 同步和异步主要用于修饰方法.当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调 ...
- async/await异步处理demo
async/await异步处理demo 下载地址: async/await异步处理demo
- async/await 异步编程(转载)
转载地址:http://www.cnblogs.com/teroy/p/4015461.html 前言 最近在学习Web Api框架的时候接触到了async/await,这个特性是.NET 4.5引入 ...
- async/await 异步编程
前言 最近在学习Web Api框架的时候接触到了async/await,这个特性是.NET 4.5引入的,由于之前对于异步编程不是很了解,所以花费了一些时间学习一下相关的知识,并整理成这篇博客,如果在 ...
- C# Async/Await异步函数原理
原理 与同步函数相比,CLR在执行异步函数时有几个不同的特点: 1. 并非一次完成,而且分多次完成 2. 并非由同一个线程完成,而是线程池每次动态分配一个线程来处理: 结合 ...
- asp.net ashx 一般处理程序 使用async await异步直接 copy可用哦
以前一直很懒 碰到ashx要用await异步就绕开 用aspx 或者mvc异步控制器 这次公司需要 我查了国内的文章基本都不能简单copy来处理一堆错关键的过程中函数BeginProcess ...
随机推荐
- JDBC 插入大批量数据
时不时会有大量数据的插入操作,方式有多种,效率不同: 1. statement 2. prepareStatement 3. statement/prepareStatement + batch 4. ...
- Outlook 配置qq邮箱账号
最近想用Outlook 2013管理QQ邮件,配置好久都没有成功,结果最后发现第三方登陆QQ邮箱不使用QQ密码,而是使用一个叫”授权码”的东西.(用户名自动生成的,授权码就填这,报错后填会测试不通过) ...
- angular之自定义管道
1,装了angular2 的 cli之后,cmd中命令建立个管道文件 ng g p <name>; 如建一个在pipe文件中建一个add.pipe.ts文件 可以这么么写 ng g p p ...
- go-statsd项目
linux命令: 进程:top 收包丢包:netstat -su[c持续输出] go tool pprof: 我们可以使用go tool pprof命令来交互式的访问概要文件的内容.命令将会分析指定的 ...
- Jsonnet-PHP v1.3.0 发布,支持 PHP 7 使用 Jsonnet
JsonNet-PHP 是 Google Jsonnet 对 PHP的支持扩展. pecl: http://pecl.php.net/package/jsonnet github: https://g ...
- 项目使用文档管理:MediaWiki安装及使用入门
MediaWiki是著名的开源wiki引擎,全球最大的wiki项目维基百科(百科词条协作系统)是使用MediaWiki的成功范例,MediaWiki的最大作用在于对知识的归档,可用于构建企业/个人知识 ...
- Linux命令详解-touch
linux的touch命令不常用,一般在使用make的时候可能会用到,用来修改文件时间戳,或者新建一个不存在的文件. 1.命令格式: touch [选项]... 文件... 2.命令参数: -a 或- ...
- POJ 3167 Cow Pattern ★(KMP好题)
题意 给你一个数字序列S,再给一个数字序列pattern,S和pattern中的数字都是1到s(s<=25).每个序列里的数字都有个排名,也就是第几小,现在我们要用pattern来匹配S.在本题 ...
- RxJava 中的Map函数原理分析
首先看一段Map函数的使用代码: Observable.create(new Observable.OnSubscribe<Integer>() { @Override public vo ...
- qml 知识积累
本文仅仅只是记录,怕日后遗忘. ListView相关应用 https://www.cnblogs.com/SaveDictator/p/8192391.html ListView 清空操作 https ...