ChatGPT解决这个技术问题 Extra ChatGPT

'await' 有效,但调用 task.Result 挂起/死锁

我有以下四个测试,最后一个在我运行时挂起。为什么会这样:

[Test]
public void CheckOnceResultTest()
{
    Assert.IsTrue(CheckStatus().Result);
}

[Test]
public async void CheckOnceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceResultTest()
{
    Assert.IsTrue(CheckStatus().Result); // This hangs
    Assert.IsTrue(await CheckStatus());
}

private async Task<bool> CheckStatus()
{
    var restClient = new RestClient(@"https://api.test.nordnet.se/next/1");
    Task<IRestResponse<DummyServiceStatus>> restResponse = restClient.ExecuteTaskAsync<DummyServiceStatus>(new RestRequest(Method.GET));
    IRestResponse<DummyServiceStatus> response = await restResponse;
    return response.Data.SystemRunning;
}

我将此扩展方法用于 restsharp RestClient

public static class RestClientExt
{
    public static Task<IRestResponse<T>> ExecuteTaskAsync<T>(this RestClient client, IRestRequest request) where T : new()
    {
        var tcs = new TaskCompletionSource<IRestResponse<T>>();
        RestRequestAsyncHandle asyncHandle = client.ExecuteAsync<T>(request, tcs.SetResult);
        return tcs.Task;
    }
}
public class DummyServiceStatus
{
    public string Message { get; set; }
    public bool ValidVersion { get; set; }
    public bool SystemRunning { get; set; }
    public bool SkipPhrase { get; set; }
    public long Timestamp { get; set; }
}

为什么最后一个测试挂起?

您应该避免从异步方法返回 void。它只是为了向后兼容现有的事件处理程序,主要是在接口代码中。如果你的异步方法没有返回任何东西,它应该返回 Task。我在使用 MSTest 时遇到了很多问题,并且返回异步测试无效。
@ghord:MSTest 根本不支持 async void 单元测试方法;他们根本行不通。但是,NUnit 可以。也就是说,我同意优先选择 async Task 而不是 async void 的一般原则。
@StephenCleary 是的,尽管它在 VS2012 的测试版中被允许,这会导致各种问题。

H
Herman Schoenfeld

通过异步方法获取值:

var result = Task.Run(() => asyncGetValue()).Result;

同步调用异步方法

Task.Run( () => asyncMethod()).Wait();

使用 Task.Run 不会出现死锁问题。


-1 用于鼓励使用 async void 单元测试方法并从被测系统中删除 SynchronizationContext 提供的同线程保证。
@StephenCleary:没有“鼓励”异步无效。它只是使用有效的 c# 构造来解决死锁问题。上面的代码片段是 OP 问题不可或缺的简单解决方法。 Stackoverflow 是关于问题的解决方案,而不是冗长的自我推销。
@StephenCleary:您的文章并没有真正阐明解决方案(至少不清楚),即使您有解决方案,您也会间接使用此类构造。我的解决方案没有明确使用上下文,那又如何?关键是,我的作品是单行的。不需要两篇博文和数千字来解决问题。注意:我什至不使用 async void,所以我真的不知道你在说什么。你在我简洁而正确的答案中的任何地方看到“async void”吗?
@HermanSchoenfeld,如果您将原因添加到如何,我相信您的回答会受益匪浅。
我知道这有点晚了,但您应该使用 .GetAwaiter().GetResult() 而不是 .Result,这样任何 Exception 都不会被包装。
S
Stephen Cleary

您遇到了我描述的 on my blogin an MSDN article 的标准死锁情况:async 方法试图将其继续调度到被调用 Result 阻塞的线程上。

在这种情况下,您的 SynchronizationContext 是 NUnit 用来执行 async void 测试方法的那个。我会尝试改用 async Task 测试方法。


更改为异步任务有效,现在我需要阅读您的链接内容几次,先生。
@StephenCleary 如果我必须在构造函数中调用异步方法怎么办?构造函数不能是异步的。
@StephenCleary 在您对 SO 和您的文章的几乎所有回复中,我所看到的您所谈论的只是将 Wait() 替换为调用方法 async。但对我来说,这似乎将问题推向了上游。在某些时候,something 必须被同步管理。如果我的函数是故意同步的,因为它使用 Task.Run() 管理长时间运行的工作线程,该怎么办?我如何等待它完成而不在我的 NUnit 测试中死锁?
@void.pointer:At some point, something has to be managed synchronously. - 一点也不。对于 UI 应用程序,入口点可以是 async void 事件处理程序。对于服务器应用程序,入口点可以是 async Task<T> 操作。最好对两者都使用 async 以避免阻塞线程。你可以让你的 NUnit 测试同步或异步;如果是异步的,则将其设为 async Task 而不是 async void。如果它是同步的,它不应该有 SynchronizationContext 所以不应该有死锁。
我在尝试解决死锁(任务卡在 Scheduled 状态)时遇到了这个问题,我发现让事件处理程序 async 没有没有帮助。我必须按照 Task.Run(() => asyncGetValue()).Result 包装
V
Vladimir

ConfigureAwait(false) 添加到此行可以避免死锁:

IRestResponse<DummyServiceStatus> response = await restResponse;

=>

IRestResponse<DummyServiceStatus> response = await restResponse.ConfigureAwait(false);

我在我的博文中描述了这个陷阱Pitfalls of async/await


a
abatishchev

您正在使用 Task.Result 属性阻止 UI。他们在 MSDN Documentation 中明确提到,

“Result 属性是一个阻塞属性。如果您在其任务完成之前尝试访问它,当前处于活动状态的线程将被阻塞,直到任务完成并且该值可用。在大多数情况下,您应该使用 Await 访问该值或等待而不是直接访问该属性。”

这种情况的最佳解决方案是从方法中删除 await 和 async 并仅使用返回结果的 Task 。它不会弄乱你的执行顺序。


O
Ogglas

@HermanSchoenfeld 给出的答案的补充。不幸的是,下面的引用不是真的:

使用 Task.Run 不会出现死锁问题。

public String GetSqlConnString(RubrikkUser user, RubrikkDb db) 
{ 
    // deadlock if called from threadpool, 
    // works fine on UI thread, works fine from console main 
    return Task.Run(() => 
        GetSqlConnStringAsync(user, db)).Result; 
}

执行被包裹在一个Task.Run中,这将在线程池上调度任务,阻塞调用线程。这没关系,只要调用线程不是线程池线程。如果调用线程来自线程池,则会发生以下灾难:一个新任务排队到队列的末尾,并且最终将执行该任务的线程池线程被阻塞,直到该任务被执行。

在库代码中没有简单的解决方案,因为您无法假设在什么上下文中调用您的代码。最好的解决方案是只从异步代码中调用异步代码,从同步方法中阻止同步 API,不要混合它们。

资源:

https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d


P
Peter Mortensen

如果您没有收到任何回调或控件挂起,则在调用服务/API 异步函数后,您必须配置 Context 以在相同的调用上下文上返回结果。

使用TestAsync().ConfigureAwait(continueOnCapturedContext: false);

您只会在 Web 应用程序中遇到此问题,而不会在 static void main 中遇到此问题。


ConfigureAwait 通过不在原始线程上下文中运行来避免在某些情况下发生死锁。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅