【问题标题】:Recursive WinRT Async Issues递归 WinRT 异步问题
【发布时间】:2012-03-08 03:50:38
【问题描述】:

我有一些代码可以做这样的事情:

abstract class Data
{
    Data(string name, bool load) { if (load) { Load().Wait(); }
    abstract Task Load();
}

class XmlData : Data
{
    XmlData(string name, bool load = true) : base(name, load) {}
    override async Task Load()
    {
        var file = await GetFileAsync(...);
        var xmlDoc = await LoadXmlDocAsync(file);
        ProcessXml(xmlDoc);
    }
    void ProcessXml(XmlDocument xmlDoc)
    {
        foreach (var element in xmlDoc.Nodes)
        {
            if (element.NodeName == "something")
                new XmlData(element.NodeText);
        }
    }
}

我似乎(有时)遇到了奇怪的时间问题,最终将代码挂在 GetFileAsync(...) 处。这是由调用的递归性质引起的吗?当我将所有 await 调用更改为实际执行 .Wait() 以完成它们并从本质上摆脱调用的所有异步性质时,我的代码执行得很好。

【问题讨论】:

  • 当我附加调试器并中断时,它只是坐着,等待 Load.Wait()。我现在没有它。但据我记得,没有其他任何东西在执行。它只是在等待加载,但似乎没有其他任何事情发生。
  • @gamernb 如果是这种情况,这几乎可以保证在捕获的上下文中等待死锁。看我的回答。
  • 在构造函数中调用虚方法通常是个坏主意(例如参见stackoverflow.com/a/448272/224370
  • 是“Load.Wait();”实际上编码为“Load().Wait();” ?

标签: c# asynchronous recursion windows-runtime async-await


【解决方案1】:

这是由调用的递归性质引起的吗?当我将所有 await 调用更改为实际执行 .Wait() 以完成它们并从本质上摆脱调用的所有异步性质时,我的代码执行得很好。

这真的取决于 -

最可能的罪魁祸首是调用者以某种方式阻塞了用户界面线程(通过调用 Wait() 等)。在这种情况下,await 的默认行为是捕获调用同步上下文,并将结果发布回该上下文。

但是,如果调用者正在使用该上下文,则可能会出现死锁。

很可能是这种情况,并且是由这行代码引起的:

Data(string name, bool load) { if (load) { Load.Wait(); }

通过使您的库代码(如这个 XmlData 类)显式使用调用同步上下文,可以轻松避免这种情况。这通常只需要用户界面代码。通过避免捕获,您可以做两件事。首先,您可以提高整体性能(通常是显着的),其次,您可以避免这种死锁情况。

这可以通过使用ConfigureAwait 并像这样更改您的代码来完成:

override async Task Load()
{
    var file = await GetFileAsync.(...).ConfigureAwait(false);
    var xmlDoc = await LoadXmlDocAsync(file).ConfigureAwait(false);
    ProcessXml(xmlDoc);
}

话虽如此 - 我会重新考虑一下这个设计。这里确实有两个问题。

首先,您在构造函数中调用虚方法,这是相当危险的,应尽可能避免,因为它可能导致异常问题。

其次,通过将 this 放入带有块的构造函数中,您可以将整个异步操作转换为同步操作。相反,我建议重新考虑整个事情。

也许您可以重新设计它以创建某种形式的工厂,它会返回异步加载的数据?这可以像将用于创建的公共 api 设置为返回 Task<Data> 的工厂方法一样简单,甚至可以是通用的 public async Task<TData> Create<TData>(string name) where TData : Data 方法,这将允许您保持构造和加载异步并完全避免阻塞。

【讨论】:

  • 我认为在第一个 Task 上调用 ConfigureAwait() 就足够了,因为第二行将在没有同步上下文的情况下执行。但是这样做可能会更健壮(例如,如果我决定稍后同步执行第一个操作)。
  • @svick True - 大部分情况下。我更喜欢这个,因为如果你在两者之间引入任何可能改变这种行为的东西会更容易......另外,如果GetFileAsync 已经完成并同步运行(即:如果它使用缓存任务),那么第二个@ 987654329@ 仍然是必需的。
  • @svick 这只是因为不能保证第一个任务会改变上下文,因为它总是可以同步完成并且永远不会切换状态。
  • 我有机会进行快速测试,在该调用路径中的所有等待上调用 ConfigureAwait(false)。这似乎没有什么不同。不过我没有机会再调试了。
猜你喜欢
  • 2017-08-11
  • 1970-01-01
  • 2011-09-20
  • 1970-01-01
  • 2021-08-28
  • 2015-10-30
  • 2017-06-24
  • 2016-02-21
  • 2017-10-28
相关资源
最近更新 更多