Skip to content

async await 理解

异步编程之Async,Await和ConfigureAwait的关系 - Leon_Chaunce - 博客园 (cnblogs.com)

理解C#中的ConfigureAwait - xiaoxiaotank - 博客园 (cnblogs.com)

【译】ConfigureAwait FAQ - 知乎 (zhihu.com)

Winform同步调用异步函数死锁原因分析、为什么要用异步 - 江边飞鸟 - 博客园 (cnblogs.com)

必死代码

async/await Task 被调用时会在调用方的上下文中(相同线程),await 在代码空闲时让出当前线程给其它代码运行(减少线程切换的开销)。

所以,async/await task 不是并行/多核编程。 async/await 不引发线程切换

async/await 调用完成后会尝试返回当前调用上下文 (调用 SynchronizationContext.Current.post )。这就是同步调用异步方法时产生死锁的原因

因为 .net core 已经取消了 SynchronizationContext, 所以没有死锁问题

private async Task<int> TaskCoreAsync(int trytimes = 5)
{
    // 3.

    for (var idx = 0; idx < trytimes; idx++)
    {
        await Task.Delay(TimeSpan.FromSeconds(0.5)); // 4,5
    }
}

void OnFormOnLoad(object sender, EventArgs args) // 1. 
{
    var task = this.TaskCoreAsync(10); // 2. 
    task.Wait(); 
}

## 解释一下, 
    ## 1. OnFormOnLoad 中为同步方法,比如线程编号为 14, 
    ## 2. 这时以同步方式调用 TaskCoreAsync 并在线程 14 上等待
    ## 3. TaskCoreAsync 在调用者线程下运行 , 线程 14
    ## 4. 在调用 14 下调用 Task.Delay
    ## 5. Task.Delay 调用完成尝试返回当前线程 14 ,但是因为 2 已经在线程 14 上等待,线程卡死

不会死锁的方案

## 方法 1
    // 从事件开始至结束都使用 async/await 就不会死锁,而且上下文都在主线程上
    async Task OnFormOnLoad(object sender, EventArgs args) + var result = await this.TaskCoreAsync(10); // 这样调用不会死锁

## 方法 2
    // 调用 ConfigureAwait(false), 不强制返回当前上下文
    // 1. 线程 11 
    await this.SayHelloAsync().ConfigureAwait(false); // 2. SayHelloAsync 内部 线程11 
    // 3. 如果有其它在线程 11 等待, 则当前上下文可能在线程 13 
    #### 特别注意
        如果 SayHelloAsync 中调用 async/await ,那也要 ConfigureAwait(false), 因为该函数也会要求返回当前线程
        简单说就是所有 async/await 调用最好都调用 ConfigureAwait(false)

## 方法 3
    var task = Task.Run(() => this.TaskCoreAsync(10)); // 这样调用不会死锁
    var task = Task.Run(async () => await this.TaskCoreAsync(10)); // 这样调用不会死锁

## 注意 4
    // 这样会死锁,因为 TaskCoreAsync 还是在当前上下文中工作的 
    async Task OnFormOnLoad(object sender, EventArgs args) + var task = this.TaskCoreAsync(10) + task.Wait(); // 死锁

.net core

听说 ~~~ 已经取消 SynchronizationContext 所以没有死锁问题 ~~~ , 假的。某一些类库可能没有,不要做这个假设

处理细节

TASK 处理

SynchronizationContext

提供一个同步上下文。在需要的时候,可以使用该上下文进行一些调用,比如在最初始的上下文中执行命令。使用方法就是调用 SynchronizationContext.Post

  • 默认实现是调用 ThreadPool.QueueUserWorkItem , 丢到线程池中处理

  • winform 实现 WindowsFormsSynchronizationContext 调用 Control.BeginInvoke, 丢到 UI 消息队列中处理 (即 UI 线程中处理)

  • WPF 实现 DispatcherSynchronizationContext 调用 Dispatcher.BeginInvoke 送到 UI 线程处理

  • WinRT 也是一样,调用 CoreDispatcher 送到 UI 线程处理

获取当前上下文 SynchronizationContext.Current

设置当前上下文 SynchronizationContext.SetSynchronizationContext

一个简单例子

public void DoWork(Action worker, Action completion)
{
    SynchronizationContext sc = SynchronizationContext.Current; // 在调用前获取上下文
    ThreadPool.QueueUserWorkItem(_ =>
    {
        try 
        { 
            worker();
        }
        finally 
        { 
            sc.Post(_ => completion(), null);  // 调用完成后,简单调用上下文的 Post
        }
    });
}

TaskScheduler

提供一个任务排序工作机制,即调度工作。使用方法是调用 TaskScheduler.QueueTask

  • 默认 TaskScheduler.Default 返回 ThreadPoolTaskScheduler, 丢到线程池中处理
  • System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ExclusiveScheduler 排它调度,一次只运行一个 task
  • System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentScheduler 并发调度(初始化时要指定最大并发数)
  • TaskScheduler.FromCurrentSynchronizationContext 返回 SynchronizationContextTaskScheduler, 内部调用 SynchronizationContext.Current.Post 将任务丢进去处理

获取当前调度器 TaskScheduler.Current

不提供设置功能

一个简单例子

var cesp = new ConcurrentExclusiveSchedulerPair(); // 1. 新建一个调度器
Task.Factory.StartNew(() =>
{
    // 3. task 中 TaskScheduler.Current 即为传入的调度器
    Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler);  // 这里为 true
}, default, TaskCreationOptions.None, cesp.ExclusiveScheduler) // 2. 创建 task 时提供调度器 
.Wait();

async/await

返回当前上下文调用

当我们在 ui 线程中工作时(比如下载页面),希望工作完成后在 ui 线程中设置界面元素

实现有如下 3 个方法,async/await 最为优雅

## 方法 1 
private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
    SynchronizationContext sc = SynchronizationContext.Current; // 1. 获取当前上下文,默认是 ui 线程上下文,
    s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
    {
        // 2. 工作完成后使用保存的当前工作上下文进行 post, 本例中为 ui 线程中
        sc.Post(delegate { downloadBtn.Content = downloadTask.Result;}, null);
    });
}

## 方法 2
private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
    // 1. 创建一个 SynchronizationContext.Current 对应的调度器
    var scheduler = TaskScheduler.FromCurrentSynchronizationContext(); 
    s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
    {
        // 当前 task 运行在 ui 线程中,因为是 1 中的 SynchronizationContext.Current.post
        downloadBtn.Content = downloadTask.Result;
    },  scheduler); // 2. 指定 ContinueWith 的 task 的调度器
}

## 方法 3 
见 《async/await  调用 》

async/await 调用 及 ConfigureAwait(false)

private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
    // 1. await 调用前保存 SynchronizationContext.Current 与 TaskScheduler.Current
    // 2. 调用 Task.GetAwaiter() 返回 TaskAwaiter<T>,在其中保存上下文与调度器 
    // 3. TaskAwaiter 挂起回调,在等待对象完成后在保存的上下文或调度器中运行
    // 4. 大致理解,有上下文,在上下文中 post 运行,如果上下文在调度器中运行(线程池)
    string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
    downloadBtn.Content = text;
} 

## 调用 ConfigureAwait(false) 改变如下行为 
// 上面的 4 中放弃保存的上下文或调度器,这样就避免强制在原始上下文或调度程序中进行回调,
// await task().ConfigureAwait(false) ,  ConfigureAwait(false) 中返回的 TaskAwaiter 进行了特别处理

    ## 好处 1 不在查询当前上下文,不进行排队,直接调用回调,提升性能 
    ## 好处 2 避免了死锁,因为运行上下文可能会有调用并发限制,当前线程中正在调用 TaskAwaiter.Wait(), 而 await 的代码调用完成后再次调用上下文 post 尝试再次返回当前线程中执行,这时,因为并发限制,产生死锁。就算并发数有多个,因为多次调用 await 会导致多次等待直到锁死

调用 ConfigureAwait(true)

一般不要主动调用,它是默认方式

调用 ConfigureAwait(false)

  • 应用程序中一般不使用 ConfigureAwait(false) ,因为大多数时候希望回到当前上下文中。比如在事件处理中的 ui 线程中,调用完成后,一般也希望再次回到 ui 线程中进行后续代码(比如设置界面元素),如果调用 ConfigureAwait(false) ,那么可能会回到其它线程中,设置界面元素会出错

csharp private async void downloadBtn_Click(object sender, RoutedEventArgs e) { // 这里就不要调用 ConfigureAwait(false) , 如果调用的话 downloadBtn.Content = text 会报异常 string text = await s_httpClient.GetStringAsync("http://example.com/currenttime"); downloadBtn.Content = text; }

  • 通用库中一般要调用 ConfigureAwait(false)。因为通用库不希望与界面 UI 有关,所以 所有 await 中都要调用 ConfigureAwait(false) 。否则被 ui 线程调用 时地,一不小心就会死锁

markdown 也可能会被传入一些回调函数,而这些回调整函数可能是在 ui 上下文中运行的。这样就会有漏洞

  • ConfigureAwait(false) 并不总是不回到当前上下文中,比如 await taskAsync().ConfigureAwait(false), 而 taskAsync() 返回的 task 已经完成任务,则不会进行排队等待,代码直接同步运行下去。这时就在相同的上下文中运行了

  • 方法第一个 await 使用 ConfigureAwait(false) ,其它不使用可以吗, 最好不要。

markdown 除非,第一个 await 后明确不会回到当前上下文,而且其它 await 也不会引用最初的上下文。所以最好不要,因为不小心就会回到最初的上下文中(UI 线程)

  • 通过 Task.Run 防止死锁。 可以,就是会有性能损失。而且注意不要将当前上下文引用到 Task 中,比如指定 TaskScheduler.FromCurrentSynchronizationContext 为 Scheduler (这个我没有测试,应该是会死锁)

``` csharp Task.Run(async delegate { await SomethingAsync(); // await 后不会找到原始上下文,没必要 ConfigureAwait(false) });

```

  • 调用 SynchronizationContext.SetSynchronizationContext(null) 是否可以防止死锁 。 最好不要这样。因为该代码不影响 TaskScheduler, 所以还是有可能会死锁。而且见如下代码

csharp var old = SynchronizationContext.Current; ## 1. 保存当前上下文, 线程 1 SynchronizationContext.SetSynchronizationContext(null); ## 2. 将当前上下文置空, 线程 1 try { await t; ## 3. 等待后返回的可能不是当前上下文了, 线程 1 ## 4. 线程就不是前面的上下文了,线程 14 } finally { ## 5. 希望重置回原始上下文,但该代码在线程 14 下运行,当前环境非最初始的上下文了,所以可能会有意外发生,所以不要这么调用 ## 被 set 的上下文已经不是在 1 那里 current 的上下文了 SynchronizationContext.SetSynchronizationContext(old); }

  • awaiter 的一些细节

markdown awaiters 公开IsCompleted属性、GetResult方法和OnCompleted方法(可选使用UnsafeOnCompleted方法), 其中 ConfigureAwait只会影响OnCompleted/UnsafeOnCompleted 所以调用 GetResult 不需要 ConfigureAwait, 所以 task.ConfigureAwait(false).GetAwaiter().GetResult(),则可以将其替换为task.GetAwaiter().GetResult(), 两者等价

  • 如何 await using一个IAsyncDisposable

``` csharp // 错误, 这里 c 是 ConfiguredAsyncDisposable await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false)) { ... }

// 要这么写 var c = new MyAsyncDisposableClass(); await using (c.ConfigureAwait(false)) { ... } ```