使用ConfigureAwait(false)后,Asp.Net流程仍然出现死锁

13

即使我使用了 ConfigureAwait(false),我仍然陷入死锁,以下是示例代码。

根据示例http://blog.stephencleary.com/2012/02/async-and-await.html(#避免上下文),这不应该导致死锁。

这是我的类

public class ProjectsRetriever
{
    public string GetProjects()
    {
        ...
        var projects = this.GetProjects(uri).Result;
        ...
        ...
    }

    private async Task<IEnumerable<Project>> GetProjects(Uri uri)
    {
        return await this.projectSystem.GetProjects(uri, Constants.UserName).ConfigureAwait(false);
    }
}

这个类来自一个共享库:

public class ProjectSystem
{
    public async Task<IEnumerable<Project>> GetProjects(Uri uri, string userName)
    {
        var projectClient = this.GetHttpClient<ProjectHttpClient>(uri);
        var projects = await projectClient.GetProjects();
        // code here is never hit
        ...
}

如果在共享库中的 await 调用中添加 ConfigureAwait(false),则会起作用,其中包含 HttpClient 调用:

public class ProjectSystem
{
    public async Task<IEnumerable<Project>> GetProjects(Uri uri, string userName)
    {
        var projectClient = this.GetHttpClient<ProjectHttpClient>(uri);
        var projects = await projectClient.GetProjects().ConfigureAwait(false);
        // no deadlock, resumes in a new thread.
        ...
}

我已经查看了所有找到的博客,唯一的区别是在使用httpClient.AsyncApi()调用时使用ConfigureAwait(false)有效果!?

请帮忙澄清!!!


你的标题说“即使使用了ConfigureAwait(false)也不起作用”,但是在你的代码中,你又说第二个示例可以工作。那到底是哪一个呢? - Yuval Itzchakov
4
我曾经认为,一旦在调用堆栈中的任何位置使用了ConfigureAwait(false),从那一点开始执行将不会导致死锁。它不会捕获等待的上下文。但是,如果打破您的调用和等待,您会发现在调用(和等待)GetProjects返回的任务上调用ConfigureAwait(false)之前,已经调用(和等待)了ProjectSystem.GetProjects。在我看来,最好的答案是“只提供异步API”,即使ProjectsRetriever.GetProjects()异步化。 - Stephen Cleary
我不理解这篇文章。GetProjects()(没有参数的重载)不是async并且不返回Task,因此您无法在其上使用await。这段代码甚至都不合法。 - BlueRaja - Danny Pflughoeft
但这是示例代码的一部分,试图解释问题。你声称遇到了“等待”同步方法的问题。如果该方法是同步的,你不能“等待”它,这是编译器错误。如果该方法实际上是异步的,那么你不应该使用.Result,而应该使用await。这个问题毫无意义。 - BlueRaja - Danny Pflughoeft
抱歉,我需要更好地理解你的问题。 是的,GetProjects()不是异步的。我在哪里使用它作为异步方法了吗? 关于编译器错误,这是一个有效的代码,我只是重命名了类名。如果你有问题,请让我知道。 - Suresh Tadisetty
显示剩余3条评论
3个回答

18

从评论中可以看出:

我曾认为,一旦在调用堆栈的任何位置使用ConfigureAwait(false),从该点开始执行将不会导致死锁。

我不相信黑魔法,你也不应该。始终努力理解在代码中使用某个东西时会发生什么。

当您await返回TaskTask<T>的异步方法时,Task.GetAwaiter方法会隐式捕获SynchronizationContext,并生成TaskAwaitable

一旦该同步上下文到位,并且异步方法调用完成后,TaskAwaitable尝试将继续(基本上是第一个await关键字之后的所有方法调用)元素封送到以前捕获的SynchronizationContext上(使用SynchronizationContext.Post)。如果调用线程被阻塞,等待同一方法完成,则会发生死锁

您应该问自己我是否应该为异步方法公开同步包装器?99%的时间答案是否定的。您应该使用同步API,例如WebClient提供的API。


2
我已经创建了一个库,可以轻松地插入到你的 ASP.NET 应用程序中来检测这些死锁,并帮助你跟踪它们。更多信息请访问 https://github.com/ramondeklein/deadlockdetection。 - Ramon de Klein
我通过阅读你的回答感受到了你的自信。回答得很好。 - johni
我来晚了,但文章的URL已更新为https://devblogs.microsoft.com/pfxteam/should-i-expose-synchronous-wrappers-for-asynchronous-methods/。 - Yann

7
ProjectsRetriever中使用它会导致阻塞,因为:
public class ProjectsRetriever
{
    public string GetProjects()
    {
        //querying the result blocks the thread and wait for result.
        var projects = this.GetProjects(uri).Result;
        ... //require Thread1 to continue.
        ...
    }

    private async Task<IEnumerable<Project>> GetProjects(Uri uri)
    {
        //any thread can continue the method to return result because we use ConfigureAwait(false)
        return await this.projectSystem.GetProjects(uri, Constants.UserName).ConfigureAwait(false);
    }
}

public class ProjectSystem
{
    public async Task<IEnumerable<Project>> GetProjects(Uri uri, string userName)
    {
        var projectClient = this.GetHttpClient<ProjectHttpClient>(uri);
        var projects = await projectClient.GetProjects();
        // code here is never hit because it requires Thread1 to continue its execution
        // but Thread1 is blocked in var projects = this.GetProjects(uri).Result;
        ...
}

ProjectSystem中使用时,它不会阻止程序运行,原因如下:

public class ProjectsRetriever
{
    public string GetProjects()
    {
        ...
        var projects = this.GetProjects(uri).Result;
        ...//requires Thread1 to continue
        ...
    }

    private async Task<IEnumerable<Project>> GetProjects(Uri uri)
    {
        //requires Thread1 to continue
        return await this.projectSystem.GetProjects(uri, Constants.UserName);
    }
}

public class ProjectSystem
{
    public async Task<IEnumerable<Project>> GetProjects(Uri uri, string userName)
    {
        var projectClient = this.GetHttpClient<ProjectHttpClient>(uri);
        var projects = await projectClient.GetProjects().ConfigureAwait(false);
        // no deadlock, resumes in a new thread. After this function returns, Thread1 could continue to run
}

抱歉,这部分让我感到困惑。是因为 HttpClient 正在进行 REST 调用(执行被转移,不发生在同一个上下文中)吗? - Suresh Tadisetty
@user2746890:你不理解的部分是哪个?在第一个例子中,你使用Thread1调用了await projectClient.GetProjects(),所以Thread1必须继续执行,但它被阻塞在this.GetProjects(uri).Result; - Khanh TO
我曾经认为,一旦在调用堆栈中使用了ConfigureAwait(false),从那一点开始的执行将不会导致死锁。 - Suresh Tadisetty
@user2746890:ConfigureAwait(false) 只是说明除了等待线程之外的任何线程都可以继续执行代码。 - Khanh TO
调用 Task<T>.Result 将会阻塞调用线程,直到任务完成,无论任务当前运行的线程是什么(或者它是否正在运行)。 - Paulo Morgado

1
我有同样的问题。"ConfigureAwait(false)"并不能总是避免死锁。
public class HomeController : Controller
{
    public async Task<ActionResult> Index()
    {
        // This works !
        ViewBag.Title = GetAsync().Result;

        // This cause deadlock even with "ConfigureAwait(false)" !
        ViewBag.Title = PingAsync().Result;

        return View();
    }

    public async Task<string> GetAsync()
    {
        var uri = new Uri("http://www.google.com");
        return await new HttpClient().GetStringAsync(uri).ConfigureAwait(false);
    }

    public async Task<string> PingAsync()
    {
        var pingResult = await new Ping().SendPingAsync("www.google.com", 3).ConfigureAwait(false);

        return pingResult.RoundtripTime.ToString();
    }
}

对于上面的代码,"GetAsync()" 能够工作,而 "PingAsync()" 则不能。
但是我发现,如果我将异步调用包装到一个新任务中,并等待这个任务,即使没有使用 "ConfigureAwait(false)",PingAsync() 也能正常工作。
var task = Task.Run(() => PingAsync());
task.Wait();
ViewBag.Title = task.Result;

我不知道原因,也许有人可以告诉我其中的区别。


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