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

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

发布于 2018-03-16 03:58 更新于 2018-08-19 11:10

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. 传统的异步转同步的方法有哪些?有什么坑?

安全的方法

传统方法的坑在于 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,需要其他能够创建消息循环这种线程模型的方案。不过这通常是平台相关的,需要多种实现。例如 Avalonia 在 Win32 平台上使用 GetMessage 实现等待;在 iOS 和 Android 平台上使用外部的全局循环;Mac 使用 MonoMac.AppKit 创建;Linux 下使用 GtkMainIteration 实现等待。

新方法的适用范围和优劣

事实上,虽然我们使用了消息循环,但其实也适用于控制台程序,适用于各种各样奇奇怪怪的线程 —— 无论是 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 线程使用。

本文会经常更新,请阅读原文: https://walterlv.com/post/convert-async-to-sync-by-push-frame.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏增长技术

Django实践:自定义用户系统

17020
来自专栏cs

linux 学习笔记七

来自实验楼的学习笔记,文字基本复制,粘贴。 ? 下载了一个录制gif图的软件,还不错 参考与:在Linux(Ubuntu)下超好用的录屏gif软件!!...

35950
来自专栏哲学驱动设计

实战 ASP.NET Web API

Web API 框架是一个面向 Http 协议的通信框架。相对于 WCF 而言,Web API 只面向于 Http 协议设计,而且没有 WCF 那么繁琐的配置。...

27450
来自专栏架构师之路

10w定时任务,如何高效触发超时

一、缘起 很多时候,业务有定时任务或者定时超时的需求,当任务量很大时,可能需要维护大量的timer,或者进行低效的扫描。 例如:58到家APP实时消息通道系统,...

42640
来自专栏技术博客

设计模式之四(抽象工厂模式第一回合)

首先关于抽象工厂模式的学习,我们需要慢慢的,由浅入深的进入。不能单刀直入,否则可能达不到预期学明白的目标。

11910
来自专栏GreenLeaves

C# 多线程学习系列三之CLR线程池系列之ThreadPool

1、进程和CLR的关系 一个进程可以只包含一个CLR,也可以包含多个CLR 2、CLR和AppDomain的关系 一个CLR可以包含多个AppDomain 3、...

14620
来自专栏大内老A

Enterprise Library深入解析与灵活应用(1):通过Unity Extension实现和Policy Injection Application Block的集成

Enterprise Library是微软P&P部门开发的众多Open source框架中的一个,最新的版本已经出到了4.0。由于接触Enterprise Li...

18860
来自专栏Java开发者杂谈

RocketMQ专题2:三种常用生产消费方式(顺序、广播、定时)以及顺序消费源码探究

​ 在进行常用的三种消息类型例子展示的时候,我们先来说一说RocketMQ的几个重要概念:

45610
来自专栏大内老A

通过扩展改善ASP.NET MVC的验证机制[使用篇]

ASP.NET MVC提供一种基于元数据的验证方式是我们可以将相应的验证特性应用到作为Model实体的类型或者属性/字段上,但是这依然具有很多的不足。在这篇文章...

19950
来自专栏田京昆的专栏

Memcached 与 Redis 实现的对比

memcached 和 redis,作为近些年最常用的缓存服务器,相信大家对它们再熟悉不过了。前两年还在学校时,我曾经读过它们的主要源码,如今写篇笔记从个人角度...

34.1K190

扫码关注云+社区

领取腾讯云代金券