async / await 使异步代码更容易写,因为它隐藏了很多细节。 许多这些细节都捕获在 SynchronizationContext 中,这些可能会改变异步代码的行为完全由于你执行你的代码的环境(例如WPF,Winforms,控制台或ASP.NET)所控制。 若果尝试通过忽略 SynchronizationContext 产生的影响,您可能遇到死锁和竞争条件状况。

SynchronizationContext 控制任务连续的调度方式和位置,并且有许多不同的上下文可用。 如果你正在编写一个 WPF 应用程序,构建一个网站或使用 ASP.NET 的API,你应该知道你已经使用了一个特殊的 SynchronizationContext 。

SynchronizationContext in a console application

让我们来看看控制台应用程序中的一些代码:

  1. public class ConsoleApplication
  2. {
  3. public static void Main()
  4. {
  5. Console.WriteLine($"{DateTime.Now.ToString("T")} - Starting");
  6. var t1 = ExecuteAsync(() => Library.BlockingOperation());
  7. var t2 = ExecuteAsync(() => Library.BlockingOperation()));
  8. var t3 = ExecuteAsync(() => Library.BlockingOperation()));
  9.  
  10. Task.WaitAll(t1, t2, t3);
  11. Console.WriteLine($"{DateTime.Now.ToString("T")} - Finished");
  12. Console.ReadKey();
  13. }
  14.  
  15. private static async Task ExecuteAsync(Action action)
  16. {
  17. // Execute the continuation asynchronously
  18. await Task.Yield(); // The current thread returns immediately to the caller
  19. // of this method and the rest of the code in this method
  20. // will be executed asynchronously
  21.  
  22. action();
  23.  
  24. Console.WriteLine($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}");
  25. }
  26. }

其中 Library.BlockingOperation() 是一个第三方库,我们用它来阻塞正在使用的线程。 它可以是任何阻塞操作,但是为了测试的目的,您可以使用 Thread.Sleep(2) 来代替实现。

运行程序,输出结果为:
16:39:15 - Starting
16:39:17 - Completed task on thread 11
16:39:17 - Completed task on thread 10
16:39:17 - Completed task on thread 9
16:39:17 - Finished

在示例中,我们创建三个任务阻塞线程一段时间。 Task.Yield 强制一个方法是异步的,通过调度这个语句之后的所有内容(称为_continuation_)来执行,但立即将控制权返回给调用者(Task.Yield 是告知调度者"我已处理完成,可以将执行权让给其他的线程",至于最终调用哪个线程,由调度者决定,可能下一个调度的线程还是自己本身)。 从输出中可以看出,由于 Task.Yield 所有的操作最终并行执行,总执行时间只有两秒。

SynchronizationContext in an ASP.NET application

假设我们想在 ASP.NET 应用程序中重用这个代码,我们将代码 Console.WriteLine 转换为 HttpConext.Response.Write 即可,我们可以看到页面上的输出:

  1. public class HomeController : Controller
  2. {
  3. public ActionResult Index()
  4. {
  5. HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Starting");
  6. var t1 = ExecuteAsync(() => Library.BlockingOperation()));
  7. var t2 = ExecuteAsync(() => Library.BlockingOperation()));
  8. var t3 = ExecuteAsync(() => Library.BlockingOperation()));
  9.  
  10. Task.WaitAll(t1, t2, t3);
  11. HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Finished");
  12.  
  13. return View();
  14. }
  15.  
  16. private async Task ExecuteAsync(Action action)
  17. {
  18. await Task.Yield();
  19.  
  20. action();
  21. HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}");
  22. }
  23. }

我们会发现,在浏览器中启动此页面后不会加载。 看来我们是引入了一个死锁。那么这里到底发生了什么呢?

死锁的原因是控制台应用程序调度异步操作与 ASP.NET 不同。 虽然控制台应用程序只是调度线程池上的任务,而 ASP.NET 确保同一 HTTP 请求的所有异步任务都按顺序执行。 由于 Task.Yield 将剩余的工作排队,并立即将控制权返回给调用者,因此我们在运行 Task.WaitAll 的时候有三个等待操作。 Task.WaitAll 是一个阻塞操作,类似的阻塞操作还有如 Task.Wait 或 Task.Result,因此阻止当前线程。

ASP.NET 是在线程池上调度它的任务,阻塞线程并不是导致死锁的原因。 但是由于是顺序执行,这导致不允许等待操作开始执行。 如果他们无法启动,他们将永远不能完成,被阻止的线程不能继续。

此调度机制由 SynchronizationContext 类控制。 每当我们等待任务时,在等待的操作完成后,在 await 语句(即继续)之后运行的所有内容将在当前 SynchronizationContext 上被调度。 上下文决定了如何、何时和在何处执行任务。 您可以使用静态 SynchronizationContext.Current 属性访问当前上下文,并且该属性的值在 await 语句之前和之后始终相同。

在控制台应用程序中,SynchronizationContext.Current 始终为空,这意味着连接可以由线程池中的任何空闲线程拾取,这是在第一个示例中能并行执行操作的原因。 但是在我们的 ASP.NET 控制器中有一个 AspNetSynchronizationContext,它确保前面提到的顺序处理。

要点一:

不要使用阻塞任务同步方法,如 Task.Result,Task.Wait,Task.WaitAll 或 Task.WaitAny。 控制台应用程序的 Main 方法目前是该规则唯一的例外(因为当它们获得完全异步时的行为会有所改变)。

解决方案

现在我们知道不应该使用 Task.WaitAll,让我们修复我们的控制器的 Index Action:

  1. public async Task<ActionResult> Index()
  2. {
  3. HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Starting
  4. ");
  5. var t1 = ExecuteAsync(() => Library.BlockingOperation()));
  6. var t2 = ExecuteAsync(() => Library.BlockingOperation()));
  7. var t3 = ExecuteAsync(() => Library.BlockingOperation()));
  8.  
  9. await Task.WhenAll(t1, t2, t3);
  10. HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Finished
  11. ");
  12.  
  13. return View();
  14. }

我们将 Task.WaitAll(t1,t2,t3)更改为非阻塞等待 Task.WhenAll(t1,t2,t3),这也要求我们将方法的返回类型从 ActionResult 更改为 async 任务。

更改后我们看到页面上输出如下结果:

16:41:03 - Starting
16:41:05 - Completed task on thread 60
16:41:07 - Completed task on thread 50
16:41:09 - Completed task on thread 74
16:41:09 - Finished
 
这看起来更好,但我们有另一个问题。 页面现在需要六秒的加载,而不是我们在控制台应用程序中的两秒。 输出很好地显示 AspNetSynchronizationContext 确实调度其在线程池上的工作,因为我们可以看到执行任务的不同线程。 但是由于这种上下文的顺序性质,它们不会并行运行。 虽然我们解决了死锁,我们的复制粘贴代码仍然低于在控制台应用程序中使用的效率。

要点二:

永远不要假设异步代码是以并行方式执行的,除非你显式地将其设置为并行执行。 用 Task.Run 或 Task.Factory.StartNew 调度异步代码来使他们并行运行。

第二次尝试

我们使用新的的规则:

  1. private async Task ExecuteAsync(Action action)
  2. {
  3. await Task.Yield();
  4.  
  5. action();
  6. HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}
  7. ");
  8. }

to:

  1. private async Task ExecuteAsync(Action action)
  2. {
  3. await Task.Run(action);
  4. HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}
  5. ");
  6. }

Task.Run 在没有 SynchronizationContext 的情况下在线程池上调度给定的操作。 这意味着在任务内运行的所有内容都将 SynchronizationContext.Current 设置为 null。 结果是所有入队操作都可以由任何线程自由选取,并且它们不必遵循ASP.NET上下文指定的顺序执行顺序。 这也意味着任务能够并行执行。

注意 HttpContext 不是线程安全的,因此我们不应该在 Task.Run 中访问它,因为这可能在 html 输出上产生奇怪的结果。 但是由于上下文捕获,Response.Write 被确保发生在 AspNetSynchronizationContext(这是在 await 之前的当前上下文)中,确保对 HttpContext 的序列化访问。

这次的输出结果为:

16:42:27 - Starting
16:42:29 - Completed task on thread 9
16:42:29 - Completed task on thread 12
16:42:29 - Completed task on thread 14
16:42:29 - Finished
 

不仅仅如此

SynchronizationContext 可以做的不仅仅是调度任务。 AspNetSynchronizationContext 也确保正确的用户设置在当前正在执行的线程(记住,它是在整个线程池中安排工作),它使得  HttpContext.Current 可用。
在我们的代码中这些都是没有必要的,因为我们能够使用 Controller 的 HttpContext 属性。 如果我们想要提取我们超级有用的 ExecuteAsync 到一个帮助类,这变得很明显:
  1. class AsyncHelper
  2. {
  3. public static async Task ExecuteAsync(Action action)
  4. {
  5. await Task.Run(action);
  6. HttpContext.Current.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}
  7. ");
  8. }
  9. }

我们刚刚将 HttpContext.Response 更改为静态可用的 HttpContext.Current.Response 。 这仍然可以工作,这得益于 AspNetSynchronizationContext,但如果你尝试在 Task.Run 中访问 HttpContext.Current ,你会得到一个 NullReferenceException,因为 HttpContext.Current 没有设置。

忘掉上下文

正如我们在前面的例子中看到的,上下文捕获可以非常方便。 但是在许多情况下,我们不需要为 "continuation" 恢复的上下文。 上下文捕获是有代价的,如果我们不需要它,最好避免这个附加的逻辑。 假设我们要切换到日志框架,而不是直接写入加载的网页。 我们重写我们的帮助:

  1. class AsyncHelper
  2. {
  3. public static async Task ExecuteAsync(Action action)
  4. {
  5. await Task.Run(action);
  6. Log.Info($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}");
  7. }
  8. }

现在在 await 语句之后,AspNetSynchronizationContext 中没有我们需要的东西,因此在这里不恢复它是安全的。 在等待任务之后,可以使用 ConfigureAwait(false) 禁用上下文捕获。 这将告诉等待的任务调度其当前 SynchronizationContext 的延续。 因为我们使用 Task.Run,上下文是 null,因此连接被调度在线程池上(没有顺序执行约束)。

使用 ConfigureAwait(false) 时要记住的两个细节:

  • 当使用 ConfigureAwait(false) 时,不能保证 "continuation" 将在不同的上下文中运行。 它只是告诉基础设施不恢复上下文,而不是主动切换到其他的东西(使用 Task.Run 如果你想摆脱上下文)。
  • 禁用上下文捕获仅限于使用 ConfigureAwait(false) 的 await 语句。 在下一个 await(在同一方法中,在调用方法或被调用的方法)语句中,如果没有另外说明,上下文将被再次捕获和恢复。 所以你需要添加 ConfigureAwait(false) 到所有 await 语句,以防你不依赖上下文。

TL; DR;

由于异步代码的 SynchronizationContext,异步代码在不同环境中的表现可能不同。 但是,当遵循最佳做法时,我们可以将遇到问题的几率减少到最低限度。 因此,请确保您熟悉 async/await 最佳实践并坚持使用它们。

原文: Context Matters

译文: async/await SynchronizationContext 上下文问题的更多相关文章

  1. 【译】Async/Await(三)——Aysnc/Await模式

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

  2. 抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext

    长话短说,本文带大家抓住异步编程async/await语法糖的牛鼻子: SynchronizationContext 引言 C#异步编程语法糖async/await,使开发者很容易就能编写异步代码. ...

  3. 译文:TransactionScope 与 Async/Await

    你可能不知道这一点,在 .NET Framework 4.5.0  版本中包含有一个关于 System.Transactions.TransactionScope 在与 async/await 一起工 ...

  4. (译文)学习ES6非常棒的特性——Async / Await函数

    try/catch 在使用Async/Await前,我们可能这样写: const main = (paramsA, paramsB, paramsC, done) => { funcA(para ...

  5. async/await 内幕【译文】

    C# Under the Hood: async/await 原文地址:https://www.markopapic.com/csharp-under-the-hood-async-await/ 前言 ...

  6. [译]async/await中阻塞死锁

    这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的两篇博文中翻译过来. 原文1:Don'tBlock o ...

  7. [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)

    [译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...

  8. [译]async/await中使用阻塞式代码导致死锁

    原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...

  9. 进阶篇:以IL为剑,直指async/await

    接上篇:30分钟?不需要,轻松读懂IL,这篇主要从IL入手来理解async/await的工作原理. 先简单介绍下async/await,这是.net 4.5引入的语法糖,配合Task使用可以非常优雅的 ...

随机推荐

  1. ASP.NET基础之HttpContext学习

    一:HttpContext理论知识: 1:HttpContext类它对Request.Respose.Server等等都进行了封装,并保证在整个请求周期内都可以随时随地的调用:为继承 IHttpMod ...

  2. Java SE (1)之 JFrame 组件 BorderLayout 布局

    JAVA 初期,练习SE ,桌面程序, package com.sunzhiyan; import java.awt.*; import java.awt.event.*; import javax. ...

  3. .NET设计模式(10):装饰模式(Decorator Pattern)

      .NET设计模式(10):装饰模式(Decorator Pattern)   装饰模式(Decorator Pattern) --.NET设计模式系列之十 年月..在....对于..由于使用装饰模 ...

  4. 个人博客设计:创建Sql数据库操作类。

    整体的博客框架如下 数据库操作java类如下 package com.yxq.dao; import java.sql.Connection; import java.sql.DriverManage ...

  5. RABBITMQ安装注意点

    关于 RABBITMQ的配置问题安装问题windows7 和window 10我都试了windows10安装和配置不要出现中文和空格,不然你日寒飞的心都有了ERLANG的安装也是Win7直接默认的路径 ...

  6. Apache Shiro 快速入门教程,shiro 基础教程 (这篇文章非常好)

    第一部分 什么是Apache Shiro     1.什么是 apache shiro :   Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理 ...

  7. 连接远程LINUX服务器

    远程登陆linux服务器需要下载一个软件,非常好用,名字是SecureCRT5,百度搜索有很多,如果下载不到可以联系我   运行安装包,一路下一步就可以了   安装好后,运行该软件   点击左上角第二 ...

  8. EDM推送

    一.需求描述:        日前,做了一个发送客户账单的功能,邮件模板采用自定义,生成vm文件,保存至redis,    采用jodd-mail发送邮件,查询用户账单数据,账单明细,缓存加载模板并渲 ...

  9. 24种设计模式--建造者模式【Builder Pattern】

    在一个周三,快要下班了,老大突然又拉住我,喜滋滋的告诉我“牛叉公司很满意我们做的模型,又签订了一个合同,把奔驰.宝马的车辆模型都交给我们公司制作了,不过这次又额外增加了一个新需求:汽车的启动.停止.喇 ...

  10. jQuery慢慢啃之回调(十三)

    1.callbacks.add(callbacks)//回调列表中添加一个回调或回调的集合 // a sample logging function to be added to a callback ...