当前位置: 首页 > news >正文

将 async/await 异步代码转换为安全的不会死锁的同步代码

async/await 异步模型(即 TAP Task-based Asynchronous Pattern)出现以前,有大量的同步代码存在于代码库中,以至于这些代码全部迁移到 async/await 可能有些困难。这里就免不了将一部分异步代码修改为同步代码。然而传统的迁移方式存在或多或少的问题。本文将总结这些传统方法的坑,并推出一款异步转同步的新方法,解决传统方法的这些坑。


背景问题和传统方法

  1. 为什么有些方法不容易迁移到 async/await
    • 参见微软的博客 async/await 最佳实践 Async/Await - Best Practices in Asynchronous Programming。如果某个方法从同步方法修改为异步方法(例如从 var content = file.Read() 修改为 var content = await file.ReadAsync()),那么调用此方法的整个调用链全部都要改成 async/await 才能让返回值在调用链中成功传递。
  2. 传统的异步转同步的方法有哪些?有什么坑?
    • 参见我的好朋友林德熙的博客 win10 uwp 异步转同步。文章里使用 Task.Wait() 或者 Task.Result 来获取异步方法的返回值。
    • 这种方法会阻塞调用线程。如果调用线程是 UI 线程,那么 UI 将会无响应;更严重地,如果 UI 线程使用 DispatcherSynchronizationContext(参见我的另一篇文章 DispatcherSynchronizationContext - walterlv)进行线程上下文的同步,那么极有可能会造成死锁(参见我的另一篇文章 使用 Task.Wait()?立刻死锁(deadlock))。

安全的方法

传统方法的坑在于 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 线程使用。

相关文章:

  • 屏幕上那个灰色带有数字的框是什么?看着好难受!
  • Roslyn 入门:使用 Roslyn 静态分析现有项目中的代码
  • Roslyn 入门:使用 Visual Studio 的语法可视化窗格查看和了解代码的语法树
  • 利用 ReSharper 自定义代码中的错误模式,在代码审查之前就发现并修改错误
  • 在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁
  • (C#)if (this == null)?你在逗我,this 怎么可能为 null!用 IL 编译和反编译看穿一切
  • 解决 mklink 使用中的各种坑(硬链接,软链接/符号链接,目录链接)
  • Roslyn 的确定性构建
  • 使用 MSBuild 响应文件 (rsp) 来指定 dotnet build 命令行编译时的大量参数
  • VS 编译太慢了吗?新建解决方案配置关闭一部分项目的编译
  • 流畅设计 Fluent Design System 中的光照效果 RevealBrush,WPF 也能模拟实现啦!
  • 语义版本号(Semantic Versioning)
  • 使用 GitVersion 在编译或持续构建时自动使用语义版本号(Semantic Versioning)
  • UWP 流畅设计中的光照效果(容易的 RevealBorderBrush 和不那么容易的 RevealBackgroundBrush)
  • 使用 Emit 生成 IL 代码
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • JS中 map, filter, some, every, forEach, for in, for of 用法总结
  • (三)从jvm层面了解线程的启动和停止
  • 【EOS】Cleos基础
  • classpath对获取配置文件的影响
  • DOM的那些事
  • java B2B2C 源码多租户电子商城系统-Kafka基本使用介绍
  • JDK9: 集成 Jshell 和 Maven 项目.
  • js作用域和this的理解
  • MySQL主从复制读写分离及奇怪的问题
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • Theano - 导数
  • 个人博客开发系列:评论功能之GitHub账号OAuth授权
  • 基于HAProxy的高性能缓存服务器nuster
  • 聊聊redis的数据结构的应用
  • 使用agvtool更改app version/build
  • 1.Ext JS 建立web开发工程
  • 策略 : 一文教你成为人工智能(AI)领域专家
  • 基于django的视频点播网站开发-step3-注册登录功能 ...
  • ​Kaggle X光肺炎检测比赛第二名方案解析 | CVPR 2020 Workshop
  • #传输# #传输数据判断#
  • (1/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (pojstep1.1.1)poj 1298(直叙式模拟)
  • (附源码)springboot家庭装修管理系统 毕业设计 613205
  • (强烈推荐)移动端音视频从零到上手(下)
  • (十五)devops持续集成开发——jenkins流水线构建策略配置及触发器的使用
  • (四)模仿学习-完成后台管理页面查询
  • (一)80c52学习之旅-起始篇
  • (原+转)Ubuntu16.04软件中心闪退及wifi消失
  • (转)setTimeout 和 setInterval 的区别
  • (转)socket Aio demo
  • (转)淘淘商城系列——使用Spring来管理Redis单机版和集群版
  • (转载)跟我一起学习VIM - The Life Changing Editor
  • .NET/C# 避免调试器不小心提前计算本应延迟计算的值
  • ??javascript里的变量问题
  • @ 代码随想录算法训练营第8周(C语言)|Day57(动态规划)
  • [20171106]配置客户端连接注意.txt
  • [C#]DataTable常用操作总结【转】
  • [C#]winform利用seetaface6实现C#人脸检测活体检测口罩检测年龄预测性别判断眼睛状态检测
  • [C++]C++入门--引用