async/await 死锁在使用 SynchronizationContext 时

本文关键字:SynchronizationContext await 死锁 async | 更新日期: 2023-09-27 18:26:01

根据此链接:

当您等待带有 await 关键字的方法时,编译器会代表您生成一堆代码。这样做的目的之一 操作是处理与 UI 线程的同步。关键
此功能的组件是SynchronizationContext.Current 获取当前线程的同步上下文。
SynchronizationContext.Current的填充
取决于 您所在的环境。任务
查找的GetAwaiter方法 SynchronizationContext.Current .如果当前同步上下文 不为 null,传递给该等待者的延续将 回发到该同步上下文。

当以阻塞方式使用使用新的异步语言功能的方法时,如果
您有一个可用的SynchronizationContext
当你在 以阻塞方式使用此类方法(等待任务 使用等待方法或直接从结果中获取结果 属性(,您将同时阻止主线程 时间。当任务最终在该方法中完成时 线程池,它将调用延续回发到 主线程,因为SynchronizationContext.Current是 可用和捕获。但是这里有一个问题:UI 线程 被阻止,你有一个死锁!

    public class HomeController : Controller
    {    
        public ViewResult CarsSync() 
        {
            SampleAPIClient client = new SampleAPIClient();
            var cars = client.GetCarsInAWrongWayAsync().Result;
            return View("Index", model: cars);
        }
    }
    public class SampleAPIClient 
    {
        private const string ApiUri = "http://localhost:17257/api/cars";
        public async Task<IEnumerable<Car>> GetCarsInAWrongWayAsync()
        {
            using (var client = new HttpClient()) 
            {
                var response = await client.GetAsync(ApiUri);
                // Not the best way to handle it but will do the work for demo purposes
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsAsync<IEnumerable<Car>>();
            }
        }
    }

我很难理解上面语句的粗体部分,但是当我测试上面的代码时,它按预期死锁。但是我仍然不明白为什么UI线程被阻止了?

在这种情况下,可用的SynchronizationContext是什么?是 UI 线程吗?

async/await 死锁在使用 SynchronizationContext 时

我在自己的博客文章中对此进行了完整的解释,但在这里重申一下......

默认情况下,await将捕获当前"上下文"并在该上下文上恢复其async方法。除非null,否则此上下文是SynchronizationContext.Current的,在这种情况下,它是TaskScheduler.Current的。

当您一次有一个线程SynchronizationContext并且您阻止表示异步代码的任务(例如,使用 Task.WaitTask<T>.Result 时,可能会发生死锁。请注意,导致死锁的是阻塞,而不仅仅是SynchronizationContext;适当的解决方案(几乎总是(是使调用代码异步(例如,将Task.Wait/Task<T>.Result替换为await(。在 ASP.NET 上尤其如此。

但是我仍然不明白为什么UI线程被阻止了?

您的示例在 ASP.NET 上运行;没有 UI 线程。

什么是可用的同步上下文?

当前SynchronizationContext应该是 AspNetSynchronizationContext 的实例,表示 ASP.NET 请求的上下文。此上下文一次只允许一个线程进入。


因此,演练您的示例:

当请求进入此操作时,CarsSync将在该请求上下文中开始执行。它继续到这一行:

var cars = client.GetCarsInAWrongWayAsync().Result;

本质上与此相同:

Task<IEnumerable<Car>> carsTask = client.GetCarsInAWrongWayAsync();
var cars = carsTask.Result;

因此,它继续调用 GetCarsInAWrongWayAsync ,直到它到达它的第一个await(GetAsync调用(。此时,GetCarsInAWrongWayAsync捕获其当前上下文(ASP.NET 请求上下文(并返回不完整的Task<IEnumerable<Car>>。当GetAsync下载完成后,GetCarsInAWrongWayAsync将恢复在该 ASP.NET 请求上下文上的执行,并(最终(完成它已返回的任务。

但是,一旦GetCarsInAWrongWayAsync返回未完成的任务,CarsSync就会阻塞当前线程,等待该任务完成。请注意,当前线程位于该 ASP.NET 请求上下文中,因此CarsSync将阻止GetCarsInAWrongWayAsync恢复执行,从而导致死锁。

最后要注意的是,GetCarsInAWrongWayAsync是一个OK方法。如果它使用ConfigureAwait(false)会更好,但实际上并没有CarsSync是导致死锁的方法;这是对Task<T>.Result的呼吁错误的。适当的解决方法是更改CarsSync

public class HomeController : Controller
{    
  public async Task<ViewResult> CarsSync() 
  {
    SampleAPIClient client = new SampleAPIClient();
    var cars = await client.GetCarsInAWrongWayAsync();
    return View("Index", model: cars);
  }
}

关键点是某些SynchronizationContext只允许单个线程同时运行代码。一个线程正在调用ResultWait。当异步方法想要输入时,它不能。

某些SynchronizationContext是多线程的,并且不会出现此问题。