在前段时间经常看到园子里有一些文章讨论到 ConfigureAwait, 刚好今天在微软官方博客看到了 Stephen Toub 前不久的一篇答疑 ConfigureAwait 的一篇文章, 想翻译过来.
.NET 加入 async/await 特性已经有 7 年了. 这段时间, 它蔓延的非常快, 广泛; 不只在 .NET 生态系统, 也出现在其他语言和框架中. 在 .NET 中, 他见证了许多了改进, 利用异步在其他语言结构 (additional language constructs) 方面, 提供了支持异步的 API, 在基础设施中标记 async/await 作为最基本的优化(特别是在 .NET Core 的性能和分析能力上).
然而, async/await 另一方面也带来了一个问题, 那就是 ConfigureAwait. 在这片文章中, 我会解答它们. 我尝试在这篇文章从头到尾变得更好读, 能作为一个友好的答疑清单, 能为以后提供参考.
什么是 SynchronizationContext
System.Threading.SynchronizationContext 文档表明它 "它提供一个最基本的功能, 在各种同步模型中传递同步上下文", 除此之外并无其他描述.
对于它的 99% 的使用案例, SynchronizationContext 只是一个类, 它提供一个虚拟的 Post 的方法, 它传递一个委托在异步中执行(这里面其实还有其他很多虚拟成员变量, 但是很少用到, 并且与我们这次讨论毫不相干). 这个类的 Post 仅仅只是调用 ThreadPool.QueueUserWorkItem 来异步执行前面传递的委托. 但是, 那些继承类能够覆写 Post 方法, 以至于在大多数合适的地方和时间执行.
举个例子, Windows Forms 有一个 SynchronizationContext 派生类, 它复写了 Post 方法, 就等价于 Control.BeginInvoke. 那就是说所有调用这个 Post 方法都将会引起这个委托在这个相关控件关联的线程上被调用, 它被称为 "UI 线程".Windows Forms 依靠 Win32 上的消息处理程序以及有一个 "消息循环" 运行在 UI 线程上, 它简单的等待新的消息到达来处理. 那些消息可能是鼠标移动和点击, 对于键盘输入, 系统事件, 委托等都能够被执行. 所以为 Windows Forms 应用程序的 UI 线程提供一个 SynchronizationContext 实例, 让它能够得到委托在 UI 线程上执行, 需要做的只是简单的传递它给 Post.
对于 WPF 来说也是如此. 它也有它自己的 SynchronizationContext 派生类, 覆写了 Post, 类似的, 传递一个委托给 UI 线程(与之对应 Dispatcher.BeinInvoke), 在这个例子中是 WPF Dispatcher 而不是 Windows Forms 控件.
对于 Windows 运行时(WinRT). 它同样有自己的 SynchronizationContext 派生类, 覆写 Post, 通过 CoreDispatcher 也传递委托给 UI 线程.
这不仅仅只是 "在 UI 线程上运行委托". 任何人都能实现 SynchronizationContext 来覆写 Post 来做任何事. 例如, 我不会关心线程运行委托所做的事, 但是我想确保任何在 Post 执行的我编写的 SynchronizationContext 都以一定程度的并发度执行. 我可以实现它, 用我自定义 SynchronizationContext 类, 像下面一样:
- internal sealed class MaxConcurrencySynchronizationContext: SynchronizationContext
- {
- private readonly SemaphoreSlim _semaphore;
- public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel) =>
- _semaphore = new SemaphoreSlim(maxConcurrencyLevel);
- public override void Post(SendOrPostCallback d, object state) =>
- _semaphore.WaitAsync().ContinueWith(delegate
- {
- try { d(state); } finally { _semaphore.Release(); }
- }, default, TaskContinuationOptions.None, TaskScheduler.Default);
- public override void Send(SendOrPostCallback d, object state)
- {
- _semaphore.Wait();
- try { d(state); } finally { _semaphore.Release(); }
- }
- }
事实上, 单元测试框架 xunit 提供了一个 SynchronizationContext` 与上面非常相似, 它用来限制与能够并行运行的测试相关的代码量.
所有的这些好处就根抽象一样: 它提供一个单独的 API, 它能够用来排队传递委托来处理创造者想要实现他们想要的( it provides a single API that can be used to queue a delegate for handling however the creator of the implementation desires), 而不需要知道实现的细节.
所有, 如果我们在编写类库, 并且想要进行和执行相同的工作, 那么就排队委托传递回在原来位置的 "上下文", 那么我就只需要获取它们的 SynchronizationContext, 占有它, 然后当完成我的工作时调用那个上下文中的 Post 来调用传递我想要调用的委托. 于 Windows Forms, 我不必知道我应该获取一个 Control 并且调用它的 BegeinInvoke, 或者对于 WPF, 我不用知道我应该获取一个 Dispatcher 并且调用它的 BeginInvoke, 又或是在 xunit, 我应该获取它的上下文并排队传递; 我只需要获取当前的 SynchronizationContext 并调用它. 为了这个目的, SynchronizationContext 提供一个 Currenct 属性, 为了实现上面说的, 我可以像下面这样编写代码:
- public void DoWork(Action worker, Action completion)
- {
- SynchronizationContext sc = SynchronizationContext.Current;
- ThreadPool.QueueUserWorkItem(_ => {
- try {
- worker();
- }
- finally {
- sc.Post(_ => completion(), null);
- }
- });
- }
一个框架暴露了一个自定义上下文, 它从 Current 使用了 SynchronizationContext.SetSynchronizationContext 方法.
什么是 TaskScheduler
对于 "调度器",SynchronizationContext 是一个抽象类. 并且个别的框架有时候拥有自己的抽象, System.Threading.Task 也不例外 (no exception). 当那些入队列以及执行的那些任务被委托支持(backed) 时, 它们与 System.Threading.Task.TaskScheduler 相关. 就好比 SynchronizationContext 提供一个虚拟的 Post 方法对委托的调用进行排队(稍后使用实现来通过典型的委托机制来调用委托),TaskScheduler 提供一个抽象方法 QueueTask(稍微使用实现通过 ExecuteTask 方法来调用任务).
默认的调度器会通过 TaskScheduler.Default 返回的是一个线程池, 但是它尽可能的派生自 TaskScheduler 并腹写相关的方法, 来完成以何时何地的调用任务的行为. 举个例子, 核心库包含 System.Threading.Tasks.ConcurrentExclusiveSchedulerPair 类型. 这个类的实例暴露了两个 TaskScheduler 属性, 一个调用自 ExclusiveScheduler, 另一个调用自 ConcurrentScheduler. 那些被调度到 ConcurrentScheduler 的任务可能是并行运行的, 但是在构建它时, 会受制于被受限的 ConcurrentExclusiveSchedulerPair(与前面展示的 MaxConcurrencySynchronizationContext 相似), 并且当一个任务被调度到 ExclusiveScheduler 正在运行的时候, ConcurrentScheduler` 任务将不会执行, 一次只运行一个独立任务... 这样的话, 它行为就很像一个读写锁.
- using System;
- using System.Threading.Tasks;
- class Program {
- static void Main(string[] arg)
- {
- var cesp = new ConcurrentExclusiveSchedulerPair();
- Task.Factory.StartNew(() => {
- Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler);
- }, default, TaskCreationOption.None, cesp.ExclusiveScheduler).Wait();
- }
- }
- private static readonly HttpClient s_httpClient = new HttpClient();
- private void downloadBtn_Click(object sender, RoutedEventArgs e)
- {
- s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
- {
- downloadBtn.Content = downloadTask.Result;
- }, TaskScheduler.FromCurrentSynchronizationContext());
- }
- private static readonly HttpClient s_httpClient = new HttpClient();
- private void downloadBtn_Click(object sender, RoutedEventArgs e)
- {
- SynchronizationContext sc = SynchronizationContext.Current;
- s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
- {
- sc.Post(delegate
- {
- downloadBtn.Content = downloadTask.Result;
- }, null);
- });
- }
- private static readonly HttpClient s_httpClient = new HttpClient();
- private async void downloadBtn_Click(object sender, RoutedEventArgs e)
- {
- string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
- downloadBtn.Content = text;
- }
- object scheduler = SynchronizationContext.Current;
- if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
- {
- scheduler = TaskScheduler.Current;
- }
- object scheduler = null;
- if (continueOnCapturedContext)
- {
- scheduler = SynchronizationContext.Current;
- if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
- {
- scheduler = TaskScheduler.Current;
- }
- }
- private static readonly HttpClient s_httpClient = new HttpClient();
- private async void downloadBtn_Click(object sender, RoutedEventArgs e)
- {
- string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
- downloadBtn.Content = text;
- }
- private static readonly HttpClient s_httpClient = new HttpClient();
- private async void downloadBtn_Click(object sender, RoutedEventArgs e)
- {
- string text = await s_httpClient.GetStringAsync("http://example.com/currenttime").ConfigureAwait(false); // bug
- downloadBtn.Content = text;
- }
- Task.Run(async delegate
- {
- await SomethingAsync(); // 将看不到原始上下文
- });
- Task.Run(async delegate
- {
- SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx());
- await SomethingAsync(); // will target SomeCoolSyncCtx
- });
- Task t;
- SynchronizationContext old = SynchronizationContext.Current;
- SynchronizationContext.SetSynchronizationContext(null);
- try
- {
- t = CallCodeThatUsesAwaitAsync(); // awaits in here won't see the original context
- }
- finally { SynchronizationContext.SetSynchronizationContext(old); }
- await t; // will still target the original context
- SynchronizationContext old = SynchronizationContext.Current;
- SynchronizationContext.SetSynchronizationContext(null);
- try
- {
- await t;
- }
- finally { SynchronizationContext.SetSynchronizationContext(old); }
- await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false))
- {
- ...
- }
- var c = new MyAsyncDisposableClass();
- await using (c.ConfigureAwait(false))
- {
- ...
- }
- https://github.com/dotnet/csharplang/issues/645
- https://github.com/dotnet/csharplang/issues/2542
- https://github.com/dotnet/csharplang/issues/2649
- https://github.com/dotnet/csharplang/issues/2746
来源: https://www.cnblogs.com/ms27946/p/ConfigureAwait-FAQs-In-CSharp.html