使用SynchronizationContext时,异步/等待会出现死锁问题

9
根据此链接
当您使用await关键字等待方法时,编译器会代表您生成大量代码。其中一个目的是处理与UI线程的同步。这个功能的关键组件是SynchronizationContext.Current,它获取当前线程的同步上下文。根据您所处的环境,SynchronizationContext.Current会有不同的值。Task的GetAwaiter方法查找SynchronizationContext.Current。如果当前同步上下文不为空,则传递给该awaiter的继续操作将被发布回该同步上下文。 如果以阻塞方式使用新的异步语言特性消耗方法,并且有可用的SynchronizationContext,则会出现死锁。 当您以阻塞方式使用这样的方法(使用Wait方法等待任务或直接从任务的Result属性中取结果)时,同时会阻塞主线程。最终,在线程池中完成该方法内的任务时,它将调用继续操作以发布到主线程,因为已经捕获了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线程吗?

6
这是来自这里吗?如果是的话,您可以在问题中包含一个链接并指出其中大部分是引用,这将会很好。 - Damien_The_Unbeliever
1
这并不是对你问题的直接回答,但如果你将控制器操作结果设置为 Task<ViewResult>,它可以完全异步化(这在 ASP.NET 线程使用方面具有更好的可扩展性)。 - Richard Szalay
@Damien_The_Unbeliever 是的,我添加了链接。谢谢。 - nainaigu
将引用文本设置为“引用”,这样就明显了你在引用网站。 - Liam
@Groo,非常抱歉。我的英语不好,感谢你的帮助。 - nainaigu
2个回答

17

我在我的博客中详细解释了这个问题(点击此处),但在这里再次强调...

await 默认会捕获当前的“上下文(context)”并在该上下文中恢复其 async 方法。这个上下文是 SynchronizationContext.Current,除非它为 null,这种情况下它就是 TaskScheduler.Current

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

但我还是不明白为什么UI线程被阻塞了?

您的示例正在ASP.NET上运行,因此没有UI线程。

可用的SynchronizationContext是什么?

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


因此,按照您的示例进行操作:

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

var cars = client.GetCarsInAWrongWayAsync().Result;

这本质上与这个相同:

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

因此,它继续调用GetCarsInAWrongWayAsync,直到它遇到第一个awaitGetAsync调用)。此时,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);
  }
}

2
在阅读另一篇 Stephen 的文章之后,我更清楚地理解了 async/await 导致死锁的原因。该文章链接为:https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/。 - Sergei Iashin

3
重点是,一些“同步上下文”只允许一个线程同时运行代码。当一个线程调用“Result”或“Wait”时,异步方法想要进入时就无法进入。
一些“同步上下文”是多线程的,因此不会出现这个问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接