ChatGPT解决这个技术问题 Extra ChatGPT

导致死锁的异步/等待示例

我遇到了一些使用 c# 的 async/await 关键字进行异步编程的最佳实践(我是 c# 5.0 的新手)。

给出的建议之一如下:

稳定性:了解您的同步上下文

...一些同步上下文是不可重入的和单线程的。这意味着在给定时间只能在上下文中执行一个工作单元。这方面的一个示例是 Windows UI 线程或 ASP.NET 请求上下文。在这些单线程同步上下文中,很容易让自己陷入死锁。如果您从单线程上下文中生成任务,然后在上下文中等待该任务,您的等待代码可能会阻塞后台任务。

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

如果我自己尝试剖析它,主线程会在 MyWebService.GetDataAsync(); 中生成一个新线程,但由于主线程在那里等待,它会在 GetDataAsync().Result 中等待结果。同时,说数据准备好了。为什么主线程不继续它的延续逻辑并从 GetDataAsync() 返回字符串结果?

有人可以解释一下为什么上面的例子中会出现死锁吗?我完全不知道问题是什么......

你真的确定 GetDataAsync 完成了它的工作吗?或者它被卡住导致只是锁定而不是死锁?
这是提供的示例。据我了解,它应该完成它的东西并准备好某种结果......
为什么还要等任务?您应该等待,因为您基本上失去了异步模型的所有好处。
为了补充@ToniPetrina 的观点,即使没有死锁问题,var data = GetDataAsync().Result; 也是一行代码,应该永远在您不应该阻止的上下文中完成(UI 或 ASP.NET 请求)。 即使它没有死锁,它也会在不确定的时间内阻塞线程。 所以基本上这是一个糟糕的例子。 [你需要在执行这样的代码之前离开 UI 线程,或者像 Toni 建议的那样在此处也使用 await。]

s
sritmak

看看this example,Stephen 给你一个明确的答案:

所以这就是发生的事情,从顶级方法开始(UI/ASP.NET 的 Button1_Click / MyController.Get ASP.NET):顶级方法调用 GetJsonAsync(在 UI/ASP.NET 上下文中)。 GetJsonAsync 通过调用 HttpClient.GetStringAsync(仍在上下文中)启动 REST 请求。 GetStringAsync 返回未完成的任务,表示 REST 请求未完成。 GetJsonAsync 等待 GetStringAsync 返回的任务。上下文被捕获并将用于稍后继续运行 GetJsonAsync 方法。 GetJsonAsync 返回未完成的Task,表示GetJsonAsync 方法未完成。顶级方法同步阻塞 GetJsonAsync 返回的任务。这会阻塞上下文线程。 ...最终,REST 请求将完成。这样就完成了 GetStringAsync 返回的任务。 GetJsonAsync 的延续现在已准备好运行,它等待上下文可用,以便可以在上下文中执行。僵局。顶级方法正在阻塞上下文线程,等待 GetJsonAsync 完成,而 GetJsonAsync 正在等待上下文空闲以便它可以完成。对于 UI 示例,“上下文”是 UI 上下文;对于 ASP.NET 示例,“上下文”是 ASP.NET 请求上下文。这种类型的死锁可能是由任一“上下文”引起的。

您应该阅读的另一个链接:Await, and UI, and deadlocks! Oh my!


R
Richard Garside

事实 1:GetDataAsync().Result;将在 GetDataAsync() 返回的任务完成时运行,同时它会阻塞 UI 线程

事实2:await的延续(return result.ToString())排队到UI线程执行

事实 3:GetDataAsync() 返回的任务将在其排队的延续运行时完成

事实 4:排队的延续永远不会运行,因为 UI 线程被阻塞(事实 1)

僵局!

可以通过提供的替代方案来打破僵局,以避免事实 1 或事实 2。

修复 1:避免 1,4。不要阻塞 UI 线程,而是使用 var data = await GetDataAsync(),它允许 UI 线程继续运行

修复 2:避免 2,3。将等待的延续排队到未阻塞的不同线程,例如使用 var data = Task.Run(GetDataAsync).Result,这会将延续发布到线程池线程的同步上下文。这允许 GetDataAsync() 返回的任务完成。

这在 article by Stephen Toub 中得到了很好的解释,大约在他使用 DelayAsync() 示例的一半处。


关于,var data = Task.Run(GetDataAsync).Result,这对我来说是新的。我一直认为,只要 GetDataAsync 的第一个等待被命中,外部 .Result 就会随时可用,所以 data 将始终是 default。有趣的。
C
Carl Heinrich Hancke

我只是在 ASP.NET MVC 项目中再次摆弄这个问题。当您想从 PartialView 调用 async 方法时,您不能将 PartialView 设为 async。如果你这样做,你会得到一个例外。

在要从同步方法调用 async 方法的场景中,您可以使用以下简单的解决方法:

调用前,清除 SynchronizationContext 做调用,这里不会再出现死锁了,等它完成 恢复 SynchronizationContext

例子:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}

m
marvelTracker

另一个要点是你不应该阻塞任务,并且一直使用异步来防止死锁。那么这将是所有异步而不是同步阻塞。

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

如果我希望主 (UI) 线程在任务完成之前被阻塞怎么办?或者例如在控制台应用程序中?假设我想使用只支持异步的HttpClient...如何同步使用它而不会有死锁的风险?这一定是可能的。如果 WebClient 可以这样使用(因为有同步方法)并且工作得很好,那么为什么不能用 HttpClient 来完成呢?
请参阅上面 Philip Ngan 的回答(我知道这是在此评论之后发布的):将等待的延续排队到未阻塞的不同线程,例如使用 var data = Task.Run(GetDataAsync).Result
@Dexter - 重新“如果我希望在任务完成之前阻塞主(UI)线程怎么办?” - 你真的希望 UI 线程被阻塞,这意味着用户不能做任何事情,甚至不能取消 - 还是你不想继续你正在使用的方法? “await”或“Task.ContinueWith”处理后一种情况。
@ToolmakerSteve 当然我不想继续这个方法。但是我根本不能使用等待,因为我也不能一直使用异步-在 main 中调用 HttpClient,这当然不能是异步的。然后我提到在控制台应用程序中执行所有这些操作——在这种情况下,我想要的正是前者——我什至不希望我的应用程序是多线程的。封锁一切。
O
Orace

我想到的一个解决方法是在询问结果之前对任务使用 Join 扩展方法。

代码如下所示:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

join方法在哪里:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

我对这个领域的了解还不够,看不到这个解决方案的缺点(如果有的话)


我喜欢这样,调度动作在 Redux 中效果很好。我正在学习 c# 并想知道,这有什么问题,它有效吗?为什么这被否决了?