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 线程吗?
我在自己的博客文章中对此进行了完整的解释,但在这里重申一下......
默认情况下,await
将捕获当前"上下文"并在该上下文上恢复其async
方法。除非null
,否则此上下文是SynchronizationContext.Current
的,在这种情况下,它是TaskScheduler.Current
的。
当您一次有一个线程SynchronizationContext
并且您阻止表示异步代码的任务(例如,使用 Task.Wait
或 Task<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
只允许单个线程同时运行代码。一个线程正在调用Result
或Wait
。当异步方法想要输入时,它不能。
某些SynchronizationContext
是多线程的,并且不会出现此问题。