异步编制程序中的最好做法,上下文难点

async / await 使异步代码更便于写,因为它蒙蔽了重重细节。
相当多那一个细节都捕获在 SynchronizationContext
中,这一个或然会退换异步代码的一举一动完全出于你推行你的代码的条件(举个例子WPF,Winforms,调节台或ASP.NET)所决定。
若果尝试通过忽视 SynchronizationContext
发生的熏陶,您或者境遇死锁和竞争原则情状。

初稿链接

SynchronizationContext
调控职分一而再的调整措施和岗位,况且有多数例外的上下文可用。
借使你正在编辑叁个 WPF 应用程序,创设三个网址或应用 ASP.NET
的API,你应当明白你早就选取了一个新鲜的 SynchronizationContext 。

近几来来,涌现了重重关于 Microsoft .NET Framework 4.第55中学新扩大了对 async 和 await 帮助的音信。 本文意在作为学习异步编制程序的“第二步”;作者要是您已阅读过有关那风流浪漫边的足足生机勃勃篇介绍性小说。 本文不提供别的新剧情,Stack Overflow、MSDN 论坛和
async/await FAQ 那类在线财富提供了千篇后生可畏律的建议。 本文只注重介绍一些溺水在文书档案海洋中的最棒做法。

 

本文中的最棒做法更加大程度上是“指引标准”,并不是实际上法规。 在那之中各种辅导原则都有一点例外情状。 小编将解释每一种引导标准背后的因由,以便可以明白地询问哪一天适用以致曾几何时不适用。 
1
 中计算了这几个指导规范;小编将要偏下各节中逐个研究。

SynchronizationContext in a console application

让我们来探视调节台应用程序中的一些代码:

public class ConsoleApplication
{
    public static void Main()
    {
        Console.WriteLine($"{DateTime.Now.ToString("T")} - Starting");
        var t1 = ExecuteAsync(() => Library.BlockingOperation());
        var t2 = ExecuteAsync(() => Library.BlockingOperation()));
        var t3 = ExecuteAsync(() => Library.BlockingOperation()));

        Task.WaitAll(t1, t2, t3);
        Console.WriteLine($"{DateTime.Now.ToString("T")} - Finished");
        Console.ReadKey();
    }

    private static async Task ExecuteAsync(Action action)
    {
        // Execute the continuation asynchronously
        await Task.Yield();  // The current thread returns immediately to the caller
                             // of this method and the rest of the code in this method
                             // will be executed asynchronously

        action();

        Console.WriteLine($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}");
    }
}

个中 Library.BlockingOperation(卡塔尔(英语:State of Qatar)是八个第三方库,大家用它来拥塞正在选拔的线程。
它能够是别的堵塞操作,不过为了测量检验的目标,您能够动用 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
全数的操作最终并行执行,总试行时间唯有两秒。

 

图 1 异步编制程序引导标准总括

SynchronizationContext in an ASP.NET application

假诺我们想在 ASP.NET 应用程序中录取这几个代码,大家将代码
Console.WriteLine 调换为 HttpConext.Response.Write
就可以,大家得以观看页面上的输出:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Starting");
        var t1 = ExecuteAsync(() => Library.BlockingOperation()));
        var t2 = ExecuteAsync(() => Library.BlockingOperation()));
        var t3 = ExecuteAsync(() => Library.BlockingOperation()));

        Task.WaitAll(t1, t2, t3);
        HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Finished");

        return View();
    }

    private async Task ExecuteAsync(Action action)
    {
        await Task.Yield();

        action();
        HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}");
    }
}

我们会开采,在浏览器中运行此页面后不会加载。
看来我们是引进了三个死锁。那么这里究竟产生了如何吗?

死锁的开始和结果是调整台应用程序调整异步操作与 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
方法近期是该准绳唯风流倜傥的例外(因为当它们获取完全异步时的一颦一笑会具有更改)。

 

“名称” 说明 异常
避免 Async Void 最好使用 async Task 方法而不是 async void 方法 事件处理程序
始终使用 Async 不要混合阻塞式代码和异步代码 控制台 main 方法
配置上下文 尽可能使用 ConfigureAwait(false) 需要上下文的方法

减轻方案

当今我们精通不应当运用 Task.WaitAll,让大家修复大家的调整器的 Index
Action:

public async Task<ActionResult> Index()
{
    HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Starting 
");
    var t1 = ExecuteAsync(() => Library.BlockingOperation()));
    var t2 = ExecuteAsync(() => Library.BlockingOperation()));
    var t3 = ExecuteAsync(() => Library.BlockingOperation()));

    await Task.WhenAll(t1, t2, t3);
    HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Finished 
");

    return View();
}

小编们将 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
调解异步代码来使他们相互运维。

 

避免 Async Void

Async
方法有三种或然的回到类型: Task、Task<T> 和 void,不过 async
方法的原有重临类型独有 Task 和 Task<T>。 当从同步转移为异步代码时,任何重返类型 T
的艺术都会成为再次回到 Task<T> 的 async 方法,任何重回 void
的不二诀要都会化为重回 Task 的 async 方法。 下边包车型客车代码段演示了一个重返 void
的同台方法及其豆蔻梢头致的异步方法:

 

void MyMethod()
{
  // Do synchronous work.
Thread.Sleep(1000);
}
async Task MyMethodAsync()
{
  // Do asynchronous work.
await Task.Delay(1000);
}

回到
void 的 async 方法具备一定用场: 用于扶持异步事件管理程序。 事件处理程序能够回来有个别实际类型,但无法以有关语言不奇怪职业;调用重临类型的事件管理程序特别勤奋,事件管理程序实际重回某些内容这一定义也没有太概略思。 事件管理程序本质上回来 void,由此 async 方法重回void,以便能够选拔异步事件管理程序。 不过,async void 方法的部分语义与 async
Task 或 async Task<T>
方法的语义略有不一样。

Async
void 方法具有分歧的错误管理语义。 当 async Task 或 async Task<T>
方法引发那么些时,会捕获该非常并将其内置 Task 对象上。 对于 async void
方法,未有 Task 对象,由此 async void 方法引发的别的极其都会平素在
SynchronizationContext(在 async void
方法运转时处于活动状态)上引发。 图 2 演示本质上不能捕获从 async void
方法引发的不行。

图 2 无法运用 Catch 捕获来自 Async Void 方法的极度

 

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
throw;
  }
}

可以因而对 GUI/ASP.NET 应用程序使用
AppDomain.UnhandledException
或近似的漫天破获事件观看到那个极度,可是选用这几个事件进展正规分外管理会促成不可能爱戴。

Async
void 方法具备差别的三结合语义。 重回 Task 或 Task<T> 的 async
方法能够使用 await、Task.WhenAny、Task.WhenAll 等有利地组合而成。 再次来到 void 的
async 方法未提供意气风发种简易方法,用于向调用代码文告它们已产生。 运行多少个async void 方法简单,可是规定它们曾几何时结束却不易。 Async void
方法会在开发银行和完工时通报
SynchronizationContext,但是对于健康应用程序代码来说,自定义
SynchronizationContext
是生机勃勃种复杂的缓慢解决方案。

Async
void 方法难以测量试验。 由于错误管理和组成方面包车型地铁差距,因而调用 async void
方法的单元测量检验不易编写。 MSTest 异步测验援助仅适用于重返 Task 或
Task<T> 的 async 方法。 能够安装 SynchronizationContext 来检查实验全部async void 方法皆已经到位的时日并采摘全体极其,可是只需使 async void
方法改为回去 Task,这会简单得多。

一清二楚,async void 方法与 async Task
方法比较有着多少个缺陷,不过那些措施在生机勃勃种特定情景下十三分灵光:
异步事件管理程序。 语义方面包车型地铁分化对于异步事件管理程序非常常有含义。 它们会直接在
SynchronizationContext
上迷惑那些,那好像于一块事件管理程序的作为方式。 同步事件管理程序平时是个体的,因而不能够组合或直接测量试验。 小编爱好使用的叁个艺术是尽量减少异步事件管理程序中的代码(比如,让它等待富含实际逻辑的
async Task 方法)。 上面包车型大巴代码演示了那黄金时代办法,该办法通过将 async void
方法用于事件管理程序而不就义可测量检验性:

 

private async void button1_Click(object sender, EventArgs e)
{
  await Button1ClickAsync();
}
public async Task Button1ClickAsync()
{
  // Do asynchronous work.
await Task.Delay(1000);
}

假设调用方不指望 async void
方法是异步的,则这么些办法恐怕会导致深重影响。 当重回类型是 Task
时,调用方知道它在拍卖今后的操作;当再次回到类型是 void
时,调用方可能只要方法在回届时做到。 此难点只怕会以广大意想不到方式面世。 在接口(或基类)上提供重返 void 的点子的 async
实现(或重写)常常是荒谬的。 有些事件也如若其处理程序在再次回到时成功。 贰个科学觉察的牢笼是将 async lambda 传递到应用 Action
参数的措施;在此种气象下,async lambda 再次来到 void 并连任 async void
方法的拥有毛病。 常常来说,仅当 async lambda 转换为回去 Task
的嘱托项目(比如,Func<Task>)时,才应选用 async
lambda。

小结那第二个辅导标准就是,应首荐 async Task 并不是async void。 Async Task
方法更便利落到实处错误管理、可组合性和可测量试验性。 此辅导标准的例外情形是异步事件处理程序,那类管理程序必需回到
void。 此例外境况包含逻辑上是事件管理程序的点子,固然它们字面上不是事件管理程序(比方ICommand.Execute implementations)。

第三遍尝试

咱俩接受新的的准则:

private async Task ExecuteAsync(Action action)
{
    await Task.Yield();

    action();
    HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId} 
");
}

to:

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

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

 

始终使用 Async

异步代码让自家想起了一个旧事,有私人民居房提议世界是浮动在太空中的,可是三个老妇人立刻建议质询,她声称世界坐落于三个宏伟乌龟的背上。 当此人问乌龟站在哪儿时,老妻子回答:“很掌握,年轻人,上边是数不清的幼龟!”在将合营代码转变为异步代码时,您会发觉,假使异步代码调用其余异步代码并且被其它异步代码所调用,则效果最佳— 一路向下(只怕也能够说“向上”)。 其余人已注意到异步编程的散布行为,并将其名称为“传染”或将其与尸鬼病毒举行相比。 无论是海龟照旧丧尸,千真万确的是,异步代码趋势于推动周边的代码也变为异步代码。 此行为是装有类型的异步编制程序中所固有的,而不只是新
async/await 关键字。

“始终异步”表示,在未谨严思谋后果的图景下,不应混合使用同步和异步代码。 具体来说,通过调用 Task.Wait 或 Task.Result
在异步代码上海展览中心开围堵日常十分不佳。 对于在异步编制程序方面“半途而返”的技士,那是个特地举不胜举的主题材料,他们仅仅转移一小部分应用程序,并使用一块
API 包装它,以便代码校勘与应用程序的别的部分隔断。 不幸的是,他们会遇上与死锁有关的标题。 在 MSDN
论坛、Stack Overflow
和电子邮件中回答了超级多与异步相关的难题之后,小编得以说,于今截止,那是异步初读书人在摸底底工知识之后最常提问的标题:
“为啥小编的一些异步代码死锁?”


3
 演示三个简易示例,当中二个措施发生窒碍,等待 async
方法的结果。 此代码仅在调整台应用程序中劳作非凡,可是在从 GUI 或
ASP.NET 上下文调用时会死锁。 此行为容许会令人纠缠,特别是经过调节和测试程序单步奉行时,那象征没完没了的等候。 在调用
Task.Wait
时,招致死锁的实际上原因在调用仓库中上移。

图 3 在异步代码上过不去时的宽广死锁难题

 

public static class DeadlockDemo
{
  private static async Task DelayAsync()
  {
    await Task.Delay(1000);
  }
  // This method causes a deadlock when called in a GUI or ASP.NET context.
public static void Test()
  {
    // Start the delay.
var delayTask = DelayAsync();
    // Wait for the delay to complete.
delayTask.Wait();
  }
}

这种死锁的根本原因是 await 管理上下文的形式。 暗中认可情状下,当等待未成功的 Task
时,会捕获当前“上下文”,在 Task 达成时使用该上下文恢复生机措施的推行。 此“上下文”是前段时间 SynchronizationContext(除非它是
null,这种意况下则为当前 TaskScheduler)。 GUI 和 ASP.NET
应用程序具有SynchronizationContext,它每一次仅同意八个代码区块运行。 当 await
实现时,它会尝试在捕获的内外文中实践 async 方法的剩下部分。 可是该上下文已盈盈多少个线程,该线程在(同步)等待 async
方法成功。 它们相互等待对方,从而诱致死锁。

请小心,调节台应用程序不会产生这种死锁。 它们持有线程池 SynchronizationContext
并非每一趟实践一个区块的 SynchronizationContext,由此当 await
实现时,它会在线程池线程上布署 async 方法的结余部分。 该办法能够不负众望,并成功其归来职分,由此空头支票死锁。 当程序猿编写测量试验调控台程序,观看见一些异步代码按预期格局专门的工作,然后将肖似代码移动到
GUI 或 ASP.NET
应用程序中会发生死锁,此行为间距或者会令人纠缠。

此主题材料的一流建设方案是允许异步代码通过着力代码自然扩大。 要是利用此施工方案,则会看见异步代码扩张到其入口点(平时是事件处理程序或调整器操作)。 调整台应用程序不可能完全使用此设计方案,因为 Main
方法不能够是 async。 要是 Main 方法是
async,则大概会在成功以前重回,进而招致程序甘休。 
4
 演示了指引规范的那生龙活虎例外景况: 调节台应用程序的 Main
方法是代码能够在异步方法上过不去为数十分的少的三种景况之后生可畏。

图 4 Main 方法能够调用 Task.Wait 或 Task.Result

 

class Program
{
  static void Main()
  {
    MainAsync().Wait();
  }
  static async Task MainAsync()
  {
    try
    {
      // Asynchronous implementation.
await Task.Delay(1000);
    }
    catch (Exception ex)
    {
      // Handle exceptions.
}
  }
}

同意异步代码通过中央代码扩大是一流应用方案,但是那象征需举行过多方始工作,该应用程序才具突显出异步代码的骨子里利润。 可因此二种方式逐步将大气大旨代码调换为异步代码,可是那高于了本文的限量。 在一些情形下,使用 Task.Wait 或 Task.Result
也是有匡助扩充部分改造,不过急需精通死锁难题以致错误管理难题。 作者今后认证错误管理难点,并在本文前边演示怎么样防止死锁难点。

各种Task 都会蕴藏叁个不行列表。 等待 Task
时,会再度掀起第三个可怜,因而能够捕获特定万分类型(如
InvalidOperationException)。 可是,在 Task 上行使 Task.Wait 或
Task.Result 同步拥塞时,全体非常都会用 AggregateException
包装后引发。 请再次参阅图 4。 MainAsync 中的 try/catch
会捕获特定相当类型,但是只要将 try/catch 置于 Main 中,则它会一直捕获
AggregateException。 当未有 AggregateException
时,错误管理要便于管理得多,由此作者将“全局”try/catch 置于 MainAsync
中。

从那之后,笔者亲自过问了七个与异步代码上过不去有关的主题素材:
大概的死锁和更目不暇接的错误管理。 对于在 async
方法中利用梗塞代码,也可以有叁个难点。 请考虑此轻便示例:

 

public static class NotFullyAsynchronousDemo
{
  // This method synchronously blocks a thread.
public static async Task TestNotFullyAsync()
  {
    await Task.Yield();
    Thread.Sleep(5000);
  }
}

此措施不是截然异步的。 它会即时放任,重回未到位的天职,不过当它过来执行时,会联手拥塞线程正在运营的别样内容。 借使此办法是从 GUI 上下文调用,则它会拥塞 GUI
线程;要是是从 ASP.NET 乞求上下文调用,则会卡住当前 ASP.NET
必要线程。 假使异步代码不一同窒碍,则其行事意义最好。 
5
 是将同步操作替换为异步替换的速查表。

图 5 试行操作的“异步情势”

执行以下操作… 替换以下方式… 使用以下方式
检索后台任务的结果 Task.Wait 或 Task.Result await
等待任何任务完成 Task.WaitAny await Task.WhenAny
检索多个任务的结果 Task.WaitAll await Task.WhenAll
等待一段时间 Thread.Sleep await Task.Delay

小结这第二个指点规范就是,应幸免混合使用异步代码和围堵代码。 混合异步代码和封堵代码或许会引致死锁、更复杂的错误管理及左右文线程的意外堵塞。 此引导原则的例外情形是调整台应用程序的 Main
方法,或是(假设是高端客商)管理一些异步的着力代码。

不唯有如此

SynchronizationContext 能够做的不止是调节职责。
AspNetSynchronizationContext
也保险准确的顾客设置在当下正在施行的线程(记住,它是在整整线程池中配置专业),它使得 
HttpContext.Current 可用。
在我们的代码中那几个都以从未必要的,因为大家能够使用 Controller 的
HttpContext 属性。 借使大家想要提取大家一流有用的 ExecuteAsync
到三个帮手类,这变得很天下闻名:

class AsyncHelper
{
    public static async Task ExecuteAsync(Action action)
    {
        await Task.Run(action);
        HttpContext.Current.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId} 
");
    }
}

小编们恰巧将 HttpContext.Response 更改为静态可用的
HttpContext.Current.Response 。 那依然能够干活,那得益于
AspNetSynchronizationContext,但固然您品尝在 Task.Run 中访谈HttpContext.Current ,你会收获叁个 NullReferenceException,因为
HttpContext.Current 未有安装。

 

结构上下文

在本文前面,作者大致表达了当等待未产生 Task
时暗许情形下咋样捕获“上下文”,以至此捕获的上下文用于苏醒 async
方法的推行。 
3
 中的示例演示在左右文上的重作冯妇实行如何与一块拥塞爆发冲突进而招致死锁。早先后文行为还有恐怕会诱致另三个题目 — 品质难点。 随着异步
GUI 应用程序在相连增进,只怕会发觉 async 方法的无数小零部件都在选取 GUI
线程作为其上下文。 那只怕会形成慢性,因为会由于“数不清的剪纸”而减低响应性。

若要减轻此主题素材,请尽量等待 ConfigureAwait
的结果。 上面包车型地铁代码段表达了暗许上下文行为和 ConfigureAwait
的用法:

 

async Task MyMethodAsync()
{
  // Code here runs in the original context.
await Task.Delay(1000);
  // Code here runs in the original context.
await Task.Delay(1000).ConfigureAwait(
    continueOnCapturedContext: false);
  // Code here runs without the original
  // context (in this case, on the thread pool).
}

因而接受 ConfigureAwait,能够实现一点点并行性:
有些异步代码能够与 GUI 线程并行运营,实际不是延绵不断塞入零碎的劳作。

除了这些之外质量之外,ConfigureAwait 还怀有另贰个要害方面:
它可以制止死锁。 再次思考图 3;借使向 DelayAsync
中的代码行增添“ConfigureAwait(false卡塔尔国”,则可制止死锁。 那时候,当等待达成时,它会尝试在线程池上下文中履行 async
方法的盈余部分。 该方法能够实现,并成功其归来职分,因而不设有死锁。 要是必要稳步将应用程序从风度翩翩道转移为异步,则此方法会特别有用。

万大器晚成能够在点子中的某处使用
ConfigureAwait,则提议对该方式中之后的各样 await 都使用它。 前边曾涉嫌,若是等待未到位的 Task,则会捕获上下文;如果Task 已产生,则不会捕获上下文。 在分歧硬件和网络状态下,有个别任务的完毕速度大概比预期速度更加快,须求战战栗栗管理在等候在此以前完毕的归来职务。 
6
 突显了二个改造后的演示。

图 6 管理在等待从前产生的回到任务

 

async Task MyMethodAsync()
{
  // Code here runs in the original context.
await Task.FromResult(1);
  // Code here runs in the original context.
await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false);
  // Code here runs in the original context.
var random = new Random();
  int delay = random.Next(2); // Delay is either 0 or 1
  await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false);
  // Code here might or might not run in the original context.
// The same is true when you await any Task
  // that might complete very quickly.
}

只要艺术中在 await 之后有所须求上下文的代码,则不应使用
ConfigureAwait。 对于 GUI 应用程序,满含别的操作 GUI
成分、编写数据绑定属性或在于特定于 GUI 的体系(如
Dispatcher/CoreDispatcher)的代码。 对于 ASP.NET 应用程序,那包涵别的利用
HttpContext.Current 或创设 ASP.NET
响应的代码(富含调控器操作中的重回语句)。 图 7 演示 GUI
应用程序中的一个科学普及形式:让 async
事件管理程序在措施早先时禁止使用其决定,试行某个await,然后在管理程序甘休时再一次启用其调控;因为这点,事件管理程序不能够扬弃其上下文。

图 7 让 async 事件管理程序剥夺相提并论新启用其调控

 

private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // Can't use ConfigureAwait here ...
await Task.Delay(1000);
  }
  finally
  {
    // Because we need the context here.
button1.Enabled = true;
  }
}

各种async 方法都富有友好的上下文,因而朝气蓬勃旦二个 async 方法调用另叁个 async
方法,则其上下文是独自的。 图 8 演示的代码对
7
 进行了一点点改成。

图 8 每种 async 方法都装有友好的上下文

 

private async Task HandleClickAsync()
{
  // Can use ConfigureAwait here.
await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
}
private async void button1_Click(object sender, EventArgs e)
{
  button1.Enabled = false;
  try
  {
    // Can't use ConfigureAwait here.
await HandleClickAsync();
  }
  finally
  {
    // We are back on the original context for this method.
button1.Enabled = true;
  }
}

无上下文的代码可重用性更加高。尝试在代码中隔开上下文相关代码与无上下文的代码,并尽大概缩小上下文相关代码。在
8
 中,提出将事件管理程序的具备骨干逻辑都放到一个可测量检验且无上下文的
async Task 方法中,仅在上下文相关事件管理程序中保留最小量的代码。尽管是编辑
ASP.NET
应用程序,倘若存在三个或许与桌面应用程序分享的主导库,请思谋在库代码中使用
ConfigureAwait。

小结那第多个指导原则便是,应尽量选用Configure­Await。无上下文的代码对于 GUI
应用程序具备最棒质量,是豆蔻梢头种可在应用一些 async
基本代码时幸免死锁的办法。此带领标准的例外景况是内需上下文的主意。

遗忘上下文

正如小编辈在前面的事例中看见的,上下文捕获能够十一分便利。
不过在数不清场所下,我们无需为 “continuation” 复苏的上下文。
上下文捕获是有代价的,要是大家无需它,最棒制止这些附加的逻辑。
假若大家要切换成日志框架,实际不是平昔写入加载的网页。
大家重写我们的支持:

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

未来在 await 语句之后,AspNetSynchronizationContext
中从未我们要求的东西,因而在那处不复苏它是无思无虑的。
在等候任务之后,能够行使 ConfigureAwait(false卡塔尔国 禁用上下文捕获。
那将告诉等待的职务调节其如今 SynchronizationContext 的持续。
因为咱们选择 Task.Run,上下文是
null,由此接连被调治在线程池上(未有各类实施约束)。

行使 ConfigureAwait(false卡塔尔国 时要记住的三个细节:

  • 当使用 ConfigureAwait(false卡塔尔 时,不可能保障 “continuation”
    就要分裂的上下文中运作。
    它只是报告功底设备可是来上下文,实际不是知难而进切换成其它的东西(使用
    Task.Run 若是您想脱位上下文)。
  • 禁止使用上下文捕获只限于使用 ConfigureAwait(false卡塔尔 的 await 语句。
    在下二个await(在长久以来方法中,在调用方法或被调用的办法)语句中,如果未有其它表达,上下文将被再次破获和大张旗鼓。
    所以你须求丰富 ConfigureAwait(false) 到持有 await
    语句,避防你不依附上下文。

 

打听您的工具

至于
async 和 await 有这个须求精晓的内容,那自然会有点迷失方向。
9
 是遍布难点的化解方案的神速参照他事他说加以考查。

图 9 朝齑暮盐异步难点的解决方案

问题 解决方案
创建任务以执行代码 Task.Run 或 TaskFactory.StartNew(不是 Task 构造函数或 Task.Start)
为操作或事件创建任务包装 TaskFactory.FromAsync 或 TaskCompletionSource<T>
支持取消 CancellationTokenSource 和 CancellationToken
报告进度 IProgress<T> 和 Progress<T>
处理数据流 TPL 数据流或被动扩展
同步对共享资源的访问 SemaphoreSlim
异步初始化资源 AsyncLazy<T>
异步就绪生产者/使用者结构 TPL 数据流或 AsyncCollection<T>

第二个难点是职分创制。显著,async
方法能够创制职责,那是最简易的选项。倘若需求在线程池上运营代码,请使用
Task.Run。假诺要为现存异步操作或事件创造职责包装,请使用
TaskCompletionSource<T>。下几个大规模难题是什么样处理撤除和速度报告。基类库
(BCL卡塔尔(英语:State of Qatar) 包含非常用来减轻这么些主题材料的体系:
CancellationTokenSource/CancellationToken 和
IProgress<T>/Progress<T>。异步代码应运用基于任务的异步形式(或称为
TAP,msdn.microsoft.com/library/hh873175),该情势详细表明了职分创建、撤废和速度报告。

现身的另一个主题材料是如哪个地点理异步数据流。职责很棒,可是只可以回到多少个对象何况只好产生一次。对于异步流,尚可 TPL 数据流或被动扩张 (Muranox卡塔尔(قطر‎。TPL
数据流会创制雷同于主演的“网格”。昂Corax
更做实硬和高速,可是也越加不便学习。TPL 数据流和 PAJEROx
都存有异步就绪方法,十三分适用于异步代码。

偏偏因为代码是异步的,并不意味着就安全。分享能源仍亟需直面保卫安全,由于无法在锁中等待,因而那相比复杂。下边是一个异步代码示例,该代码倘诺施行三回,则恐怕会破坏分享状态,尽管始终在同贰个线程上运维也是那样:

int
value;

Task<int> GetNextValueAsync(int current);

async
Task UpdateValueAsync()

{

 
value = await GetNextValueAsync(value);

}

 

 

 

标题在于,方法读取值并在守候时挂起和煦,当方法苏醒实施时,它要是值未改换。为了消除此主题素材,使用异步就绪 WaitAsync 重载扩充了
SemaphoreSlim 类。图 10 演示
SemaphoreSlim.WaitAsync。

图 10 SemaphoreSlim 允许异步同步

SemaphoreSlim mutex = new SemaphoreSlim(1);

int
value;

Task<int> GetNextValueAsync(int current);

async
Task UpdateValueAsync()

{

 
await mutex.WaitAsync().ConfigureAwait(false);

 
try

 
{

   
value = await GetNextValueAsync(value);

 
}

 
finally

 
{

   
mutex.Release();

 
}

}

 

 

 

异步代码平常用于早先化随后会缓存并分享的财富。未有用来此用处的放权类型,不过 斯蒂芬 Toub 开辟了
AsyncLazy<T>,其行为一定于 Task<T> 和 Lazy<T>
合二为生机勃勃。该原始类型在其博客
(bit.ly/dEN178卡塔尔(英语:State of Qatar) 上实行了介绍,何况在自小编的 AsyncEx
库 (nitoasyncex.codeplex.com卡塔尔国中提供了履新版本。

提及底,一时供给一些异步就绪数据构造。TPL 数据流提供了
BufferBlock<T>,其一言一行仿佛异步就绪临盆者/使用者队列。而 AsyncEx
提供了 AsyncCollection<T>,那是异步版本的
BlockingCollection<T>。

小编愿意本文中的教导标准和指令能拥有助于。异步真的是这几个棒的言语功效,未来就是初叶利用它的好机遇!

TL; DR;

是因为异步代码的
SynchronizationContext,异步代码在分化意况中的表现大概分裂。
然而,当根据最好做法时,大家得以将蒙受标题标概率缩小到低于限度。
因而,请确认保障您纯熟 async/await 最好实施并坚威武不能屈使用它们。

 

原文: Context
Matters

相关文章