在C#中使用ConfigureAwait(false)与GetAwaiter和GetResult仍然令人困惑。出现死锁或方法无法返回的情况。

6
我已经阅读了这篇文章:http://blog.stephencleary.com/2012/07/dont-block-on-async-code.htmldeadlock even after using ConfigureAwait(false) in Asp.Net flow的被接受的答案,但是我太愚钝了,看不出发生了什么。
我有一段代码:
private void CancelCalibration()
{
    // ...
    TaskResult closeDoorResult =  CloseLoadDoor().ConfigureAwait(false).GetAwaiter().GetResult(); 
    CalibrationState = CalibrationState.Idle;

    return;
    // ...                   
}

private async Task<TaskResult> CloseLoadDoor()
{       
    TaskResult result = await _model.CloseLoadDoor().ConfigureAwait(false);           
    return result;
}
public async Task<TaskResult> CloseLoadDoor()
    {
        TaskResult result = new TaskResult()
        {
            Explanation = "",
            Success = true
        };
        await _robotController.CloseLoadDoors().ConfigureAwait(false);
        return result;
    }
    public async Task CloseLoadDoors()
    {                         
            await Task.Run(() => _robot.CloseLoadDoors());              
    }

     public void CloseLoadDoors()
    {
   // syncronous code from here down              
   _doorController.CloseLoadDoors(_operationsManager.GetLoadDoorCalibration());                
        }

您好,这段文字的意思是:正如您所看到的,CloseLoadDoor 声明为异步方法。我认为(尤其是从上面第一篇文章中),如果我使用 ConfigureAwait(false),我可以调用一个异步方法而不会出现死锁。但事实似乎并非如此。调用 "CloseLoadDoor().ConfigureAwait(false).GetAwaiter().GetResult()" 没有返回!我正在使用 GetAwaiter.GetResult,因为 CancelCalibration 不是一个异步方法。它是通过 MVVM 模式定义的按钮处理程序。
public ICommand CancelCalibrationCommand
        => _cancelCalibrationCommand ?? (_cancelCalibrationCommand = new DelegateCommand(CancelCalibration));

如果有人告诉我可以将CancelCalibration设置为异步方法,请告诉我如何操作。我只需要在方法声明中添加async吗?然而,我仍然想知道为什么ConfigureAwait.GetAwaiter.GetResult模式会给我带来麻烦。我的理解是,GetAwaiter.GetResult是一种在不改变签名的情况下从同步方法调用异步方法的方法。 我猜我并没有真正地摆脱使用原始上下文,但我做错了什么,以及修复它的模式是什么? 谢谢, 戴夫

1
我使用所示代码(使用Task.Delay()模拟_model.CloseLoadDoor())时没有遇到死锁问题。 - GSerg
1
如果您将CancelCalibration用作委托来命令它是一个逻辑事件处理程序,并且可以标记为async void,这将允许您await CloseDoor() - JSteward
2
这个异常包括那些在逻辑上是事件处理程序但实际上并不是事件处理程序的方法(例如,ICommand.Execute 实现)。异步/等待最佳实践 - JSteward
有一种模式,不要编写无法调试的代码。异步的常见问题是,你不知道该怪铁锤还是钉子。 - Hans Passant
谢谢GSerg。我认为_model.CloseLoadDoor会立即返回结果。也许在这个例子中这是正确的做法,但我想知道如何等待门关闭的函数不标记为async。谢谢! - Dave
JSteward,实际上,将CancelCalibration标记为async并调用await _model.CloseDoor()似乎正是我最终采取的方法,并且似乎有效。但是我想知道ConfigureAwait和GetAwaiter发生了什么。也许有时我无法标记方法为async?我必须承认,我很难想到这样的情况:)。但是,在任何情况下理解ConfigureAwait和GetAwaiter似乎都是一个高尚的目标!谢谢! - Dave
1个回答

14
我认为(特别是从上面的第一篇文章中),如果我使用ConfigureAwait(false),我可以调用异步方法而不会死锁。
该文章中有一个重要的提示:
使用ConfigureAwait(false)避免死锁是一种危险的做法。您必须对阻塞代码调用的所有方法的传递闭包中的每个await都使用ConfigureAwait(false),包括所有第三方和第二方代码。使用ConfigureAwait(false)避免死锁,最多只是一个hack)。
那么,在传递闭包中是否使用ConfigureAwait(false)用于每个await? 这意味着:
- CloseLoadDoor是否为每个await使用ConfigureAwait(false)? 我们可以从发布的代码中看到它确实使用了。 - _model.CloseLoadDoor是否为每个await使用ConfigureAwait(false)? 我们无法看到。 - 由_model.CloseLoadDoor调用的每个方法是否都为每个await使用ConfigureAwait(false)? - 由_model.CloseLoadDoor调用的每个方法是否都为每个await使用ConfigureAwait(false)? - 等等。
这至少是一个严重的维护负担。我怀疑在调用堆栈的某个地方,缺少了一个ConfigureAwait(false)。

正如该注释所述:

正如本文标题指出的那样,更好的解决方案是“不要阻塞异步代码”。

换句话说,该文章的全部意图都在于"不要阻塞异步代码"。它并未提供"使用这一个巧妙的技巧可以阻塞异步代码"之类的建议。

如果你确实希望拥有同时支持同步和异步调用者的API,我建议你在我的关于棕地异步的文章中使用布尔参数hack。


顺便说一下,在代码 CloseLoadDoor().ConfigureAwait(false).GetAwaiter().GetResult() 中,ConfigureAwait 没有任何作用。它是 "configure await",而不是 "configure task"。因为那里没有 await,所以 ConfigureAwait(false) 没有影响。


1
谢谢Stephen。我希望你能检查一下这个问题。我编辑了我的代码以显示其他调用的方法。它们都使用“ConfigureAwait(false)”,除非我们切换到同步代码并使用await Task.Run(() => _robot.CloseLoadDoors());而没有使用ConfigureAwait。这可能是问题所在吗? - Dave
Stephen,你的“顺便说一下”的评论让我感到困惑。也许我在使用ConfigureAwait().GetAwaiter().GetResult()时有误?我的意图是等待CloseLoadDoor的结果。也许我没有这样做?我需要在此调用中等待吗?或者另外一个问题...如果我告诉你由于某种奇怪的原因你不能使用'await',你会如何编写对CloseLoadDoor().ConfigureAwait(false).GetAwaiter().GetResult()的调用以正确模拟使用'await'?我不明白你关于“它是configure await,而不是configure task”的评论。谢谢! - Dave
我重新阅读了你的文章:https://msdn.microsoft.com/en-us/magazine/mt238404.aspx?f=255&MSPPError=-2147217396。在我的看法中,如果我无法保证 ConfigureAwait(false) 在整个堆栈中都被使用,那么我更安全地使用 Task.Run(() => CloseLoadDoor().GetAwaiter().GetResult();!当然,在我的情况下,我可以将我的顶级方法更改为 async 并使用 await,但正如你的文章所指出的那样,有时可能无法这样做。谢谢! - Dave
3
@Dave: 是的,在 await Task.Run(...); 上缺少 ConfigureAwait(false) 可能会导致死锁。在 .ConfigureAwait(false).GetAwaiter().GetResult() 上的问题是,.ConfigureAwait(false) 在那里是没有意义的;它与没有 ConfigureAwait(false).GetAwaiter().GetResult() 完全相同。对于异步转同步,没有完美的解决方案,但将其包装在 Task.Run 中并阻塞该任务将适用于大多数代码。 - Stephen Cleary
谢谢Stephen。现在我非常清楚了。在现实世界中,这仍然让我感到非常困难,因为不能保证“await ... ConfigureAwait(false)”一直被使用。有时候可能无法看到代码(隐藏在库中),或者由于某些原因无法更改代码。我很好奇你为什么说将其包装在“Task.Run”中并阻塞它会适用于大多数代码。为什么是“大多数代码”?您在文章中是否给出了失败的示例?感谢您的关注。答案已标记为接受,并希望能帮助其他人! - Dave
显示剩余2条评论

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