【问题标题】:Task.Result/wait(..) is indefinitely waits if waited on chain of tasks have 'unwrapped' task, whereas successfully completes if 'async/await' is usedTask.Result/wait(..) 如果等待的任务链具有“未包装”任务,则无限期等待,而如果使用“异步/等待”则成功完成
【发布时间】:2020-09-03 05:51:12
【问题描述】:

环境: Windows Server 2012、.net 4.5、Visual Studio 2013。

注意:不是 UI 应用程序(因此与著名的 async/await/synchronizationcontext 问题无关)(参考:http://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Async-library-methods-should-consider-using-Task-ConfigureAwait-false-

编辑

这是一个导致死锁的错字。我在下面粘贴了导致死锁的示例 sn-p(伪)。基本上,我不是在“子任务”上编写 whenall,而是在“外部任务”上编写的:(。看起来我不应该在看电视时编写“异步”代码:)。

我保留了原始代码 sn-ps,因为它确实回答了我的第一个问题(我之前的两个代码 sn-ps 中与 async/await 和 unwrap 的区别),因此它为 Kirill 的响应提供了上下文。僵局让我分心看实际问题:)。

static void Main(string[] args)
{
    Task t = IndefinitelyBlockingTask();
    t.Wait();            
}        
static Task IndefinitelyBlockingTask()
{
    List<Task> tasks = new List<Task>();
    Task task = FooAsync();
    tasks.Add(task);
    Task<Task> continuationTask = task.ContinueWith(t =>
    {
        Task.Delay(10000);
        List<Task> childtasks = new List<Task>();
        ////get child tasks
        //now INSTEAD OF ADDING CHILD TASKS, i added outer method TASKS. Typo :(:)!
        Task wa = Task.WhenAll(tasks/*TYPO*/);
        return wa;
    }, TaskContinuationOptions.OnlyOnRanToCompletion);
    tasks.Add(continuationTask);
    Task unwrappedTask = continuationTask.Unwrap();
    tasks.Add(unwrappedTask);
    Task whenall = Task.WhenAll(tasks.ToArray());
    return whenall;
}

当“解包”延续任务添加到聚合/任务链时无限期等待的代码片段我已粘贴在下面 伪(在我的实际应用程序中的模式/成语块 - 不是示例) 代码 sn-p (#1),当 'unwrapped' 任务被添加到列表时,它会无限期地等待。而 VS Debugger 'Debugger + windows + threads' 窗口显示线程只是阻塞在 ManualResetEventSlim.Wait

与 async/await 配合使用的代码片段,并删除未包装的任务 然后我删除(在调试时随机)这个 unwrap 语句并在 lambda 中使用 async/await(请参见下文)。令人惊讶的是它有效。但我不知道为什么 :(?.

问题

  1. 在下面的代码 sn-ps 中使用 unwrap 和 async/await 的目的不是相同吗?我最初只是更喜欢 sn-p #1,因为我只是想避免生成过多的代码,因为调试器不是那么友好(特别是在异常通过链式任务传播的错误场景中 - 异常中的调用堆栈显示 movenext 而不是我的实际代码)。如果是,那是不是 TPL 的 bug?

  2. 我错过了什么?如果它们相同,则首选哪种方法?

关于调试器 + 任务窗口的注意事项“调试器 + 任务”窗口不显示任何详细信息(请注意,它在我的环境中无法正常工作(至少我的理解),因为它从不显示未计划的任务和依次显示活动任务)

代码 sn-p 无限期等待 ManualResetEventSlim.Wait

static Task IndefinitelyBlockingTask()
{
    List<Task> tasks = new List<Task>();
    Task task = FooAsync();
    tasks.Add(task);
    Task<Task> continuationTask = task.ContinueWith(t =>
    {
        List<Task> childTasks = new List<Task>();
        for (int i = 1; i <= 5; i++)
        {
            var ct = FooAsync();
            childTasks.Add(ct);
        }
        Task wa = Task.WhenAll(childTasks.ToArray());
        return wa;
    }, TaskContinuationOptions.OnlyOnRanToCompletion);
    tasks.Add(continuationTask);
    Task unwrappedTask = continuationTask.Unwrap();
    //commenting below code and using async/await in lambda works (please see below code snippet)
    tasks.Add(unwrappedTask);
    Task whenall = Task.WhenAll(tasks.ToArray());
    return whenall;
}

代码 sn-p 与 lambda 中的 async/await 配合使用,而不是展开

static Task TaskWhichWorks()
{
    List<Task> tasks = new List<Task>();
    Task task = FooAsync();
    tasks.Add(task);
    Task<Task> continuationTask = task.ContinueWith(async t =>
    {
        List<Task> childTasks = new List<Task>();
        for (int i = 1; i <= 5; i++)
        {
            var ct = FooAsync();
            childTasks.Add(ct);
        }
        Task wa = Task.WhenAll(childTasks.ToArray());
        await wa.ConfigureAwait(continueOnCapturedContext: false);
    }, TaskContinuationOptions.OnlyOnRanToCompletion);
    tasks.Add(continuationTask);
    Task whenall = Task.WhenAll(tasks.ToArray());
    return whenall;
}

显示阻塞代码的调用栈

mscorlib.dll!System.Threading.ManualResetEventSlim.Wait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)  Unknown
    mscorlib.dll!System.Threading.Tasks.Task.SpinThenBlockingWait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)    Unknown
    mscorlib.dll!System.Threading.Tasks.Task.InternalWait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)    Unknown
    mscorlib.dll!System.Threading.Tasks.Task.Wait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken)    Unknown
    mscorlib.dll!System.Threading.Tasks.Task.Wait() Unknown

【问题讨论】:

  • 您是否从 ui 应用程序运行并且 fooasync 是否包含等待?
  • @Linky:它不是“UI”应用程序。还要注意,由于“等待”,该块没有发生 - 所以我认为它与 UI/同步上下文的等待问题无关(参考:channel9.msdn.com/Series/Three-Essential-Tips-for-Async/…
  • 所以您在没有任何特殊同步上下文的服务/控制台应用程序中运行?
  • @Linky:是的,还要注意代码在使用“等待”时有效(所以我猜它与此无关)。如果我在等待的任务链中包含“未包装的延续任务”,它会阻塞。如果我删除了未包装的任务并简单地使用 await 一切都很好。
  • 这真的很奇怪。我的设置和你差不多。在控制台中使用 'await Task.Delay' 的虚拟实现运行 FooAsync 并在其上运行 'Wait()' 或 'await' 可以按预期工作,在 UI 应用程序的 UI 死锁中运行相同。很想知道答案还有什么问题。

标签: c# .net multithreading task-parallel-library async-await


【解决方案1】:

好的,让我们试着深入了解这里发生的事情。

首先要做的是:传递给您的ContinueWith 的 lambda 的差异是微不足道的:在功能上,这部分在两个示例中是相同的(至少据我所知)。

这是我用于测试的FooAsync 实现:

static Task FooAsync()
{
    return Task.Delay(500);
}

我觉得奇怪的是,使用这种实现,您的 IndefinitelyBlockingTask 花费的时间是 TaskWhichWorks 的两倍(分别为 1 秒和约 500 毫秒)。显然,由于Unwrap,行为发生了变化。

有敏锐眼光的人可能会立即发现问题,但我个人不太使用任务延续 Unwrap,所以花了一点时间才陷入困境。

这里是关键:除非你在这两种情况下都使用Unwrap 继续,否则ContinueWith 安排的任务会同步完成(并且立即 - 无论在循环中创建的任务需要多长时间)。在 lambda 中创建的任务(Task.WhenAll(childTasks.ToArray()),我们称之为内部任务)以即发即弃的方式调度并在观察时运行。

Unwrapping 从ContinueWith 返回的任务意味着内部任务不再是即发即弃的 - 它现在是执行链的一部分,当您将其添加到列表中时,外部任务 ( Task.WhenAll(tasks.ToArray())) 在内部任务完成之前无法完成。

使用ContinueWith(async () =&gt; { }) 不会改变上述行为,因为异步 lambda 返回的任务不会自动解包(想想

// These two have similar behaviour and
// are interchangeable for our purposes.
Task.Run(() => Task.Delay(500))
Task.Run(async () => await Task.Delay(500));

Task.Factory.StartNew(() => Task.Delay(500))

Task.Run 调用内置了Unwrap(参见http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs#0fb2b4d9262599b9#references); StartNew 调用不会,它返回的任务会立即完成,而无需等待内部任务。 ContinueWith 在这方面类似于 StartNew

旁注

重现使用Unwrap 时观察到的行为的另一种方法是确保在循环内创建的任务(或其延续)附加到父任务,从而导致父任务(由ContinueWith 创建)不转换到完成状态,直到所有子任务都完成。

for (int i = 1; i <= 5; i++)
{
    var ct = FooAsync().ContinueWith(_ => { }, TaskContinuationOptions.AttachedToParent);
    childTasks.Add(ct);
}

回到原来的问题

在您当前的实现中,即使您将await Task.WhenAll(tasks.ToArray()) 作为外部方法的最后一行,该方法仍会返回ContinueWith lambda 中创建的任务完成之前。即使在 ContinueWith 内部创建的任务永远不会完成(我猜这正是您的生产代码中正在发生的事情),外部方法仍然会正常返回

就是这样,上面代码的所有意外都是由愚蠢的ContinueWith 引起的,除非你使用Unwrap,否则它基本上会“失败”。 async/await 绝不是原因或治疗方法(尽管,诚然,它可以并且可能应该用于以更明智的方式重写您的方法 - 延续 难以工作会导致诸如此类的问题)。

生产中发生了什么

以上所有情况都让我相信,在您的 ContinueWith lambda 中旋转的任务之一存在死锁,导致内部 Task.WhenAll 永远无法完成生产调整。

很遗憾,您还没有发布该问题的简明重现(我想我可以为您提供上述信息,但这样做真的不是我的工作)甚至是生产代码,所以这是一样多我可以给出一个解决方案。

您没有使用伪代码观察所描述的行为这一事实应该暗示您可能最终删除了导致问题的位。如果您认为这听起来很愚蠢,那是因为确实如此,这就是为什么我最终撤回了对该问题的最初支持,尽管事实上这是我一段时间以来遇到的最奇怪的异步问题。

结论:看看你的ContinueWith lambda。

最终编辑

您坚持 Unwrapawait 做类似的事情,这是真的(不是真的,因为它最终会混淆任务组合,但有点正确 - 至少对于本示例的目的)。然而,话虽如此,您从未使用await 完全重新创建Unwrap 语义,所以该方法的行为不同真的很令人惊讶吗?这是带有awaitTaskWhichWorks,其行为类似于Unwrap 示例(当应用于您的生产代码时,它也容易受到死锁问题的影响):

static async Task TaskWhichUsedToWorkButNotAnymore()
{
    List<Task> tasks = new List<Task>();
    Task task = FooAsync();
    tasks.Add(task);
    Task<Task> continuationTask = task.ContinueWith(async t =>
    {
        List<Task> childTasks = new List<Task>();
        for (int i = 1; i <= 5; i++)
        {
            var ct = FooAsync();
            childTasks.Add(ct);
        }
        Task wa = Task.WhenAll(childTasks.ToArray());
        await wa.ConfigureAwait(continueOnCapturedContext: false);
    }, TaskContinuationOptions.OnlyOnRanToCompletion);
    tasks.Add(continuationTask);

    // Let's Unwrap the async/await way.
    // Pay attention to the return type.
    // The resulting task represents the
    // completion of the task started inside
    // (and returned by) the ContinueWith delegate.
    // Without this you have no reference, and no
    // way of waiting for, the inner task.
    Task unwrappedTask = await continuationTask;

    // Boom! This method now has the
    // same behaviour as the other one.
    tasks.Add(unwrappedTask);

    await Task.WhenAll(tasks.ToArray());

    // Another way of "unwrapping" the
    // continuation just to drive the point home.
    // This will complete immediately as the
    // continuation task as well as the task
    // started inside, and returned by the continuation
    // task, have both completed at this point.
    await await continuationTask;
}

【讨论】:

  • @Kiril, >>>内部任务不再是一劳永逸>>>外部方法仍然会很好地返回。。>>>看看你的ContinueWith lambda。” - 是的,我开始详细研究它。但令我困惑的是,如果有死锁,它应该在两种情况下都锁定为asyn/await 和 unwrap 都跟踪内部任务。
  • @Kiril,只是为了强调一个事实,是的,可以肯定的是,生产中缺少一些我计划关注的东西,如果可能的话,提供一个复制品。但是,让我感到困惑的事情是在这两种情况下都应该陷入僵局。并且“调试器+线程”不显示任何其他线程,只有我的一个线程只是在等待事件(请参阅我在问题中附加的调用堆栈)。我会尝试看看它是否在win7中重现,看看我是否可以跟踪任务(由于某种原因调试器+任务没有显示任何细节)。由于所有这些嵌套任务,它很难调试或获得精确的重现。
  • @Dreamer,您可以轻松地将tasks.Add(continuationTask.Unwrap()) 插入第二个示例,从而导致死锁。 async/await 用法在这里根本不起作用。
  • @Dreamer,堆栈一清二楚:我爬过 ReferenceSource 上的调用路径,发现讨厌的 MRES (最终通过在某处调用 Task.Wait() 导致)。这是一个典型的死锁(或者可能是一个异步死锁),我刚刚告诉你在哪里可以找到它。问题中发布的所有代码(我的意思是:all 的所有代码)都只是围绕真正的问题,不会导致僵局。
  • @Dreamer,真的没什么大不了的。我抓住了它,Carsten König 抓住了它,你也会。你似乎有正确的想法,但你把它们混在一起了。让我再提出一个指针:await Task.Delay(500).ContinueWith(_ =&gt; Task.Delay(500)).Unwrap() 实际上大致相当于await await Task.Delay(500).ContinueWith(_ =&gt; Task.Delay(500)),也大致相当于await await Task.Delay(500).ContinueWith(async _ =&gt; await Task.Delay(500))。除非我在您的代码中看到额外的 await 看起来很有趣,否则您不会重新创建 Unwrap 所做的事情。
【解决方案2】:

我已接受基里尔的回答作为实际答案,因为它帮助我解决了问题。在这里,我添加了一些可能以简洁的方式直接解决这两个问题的细节,因为现在我也对死锁进行了简洁的重现(请参阅问题的编辑版本):

一个。发生死锁是因为持续任务正在等待包含“持续任务:)”代理的所有外部任务

b.我已经粘贴了等待版本的死锁以供参考。

static void Main(string[] args)
        {
            Task withUnwrap = Unwrap_IndefinitelyBlockingTask();
            Task<Task> withAwait = AwaitVersion_IndefinitelyBlockingTask();
            withAwait.Wait();
            //withUnwrap.Wait();
        }
        static async Task<Task> AwaitVersion_IndefinitelyBlockingTask()
        {
            List<Task> tasks = new List<Task>();
            Task task = FooAsync();
            tasks.Add(task);
            Task<Task<Task>> continuationTask = task.ContinueWith(async t =>
            {
                //immediately returns with generated Task<Task> return type task 
                await Task.Delay(10000);
                List<Task> childtasks = new List<Task>();
                ////get child tasks
                //now INSTEAD OF ADDING CHILD TASKS, i added outer method TASKS. Typo :(:)!
                //!!since we added compiler generated task to outer task its deadlock!!
                Task wa = Task.WhenAll(tasks/*TYPO*/);
                await wa.ConfigureAwait(continueOnCapturedContext: false);
                return wa;
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
            tasks.Add(continuationTask);
            //Task unwrappedTask = continuationTask.Unwrap();
            Task<Task> awaitedComiplerGeneratedTaskOfContinuationTask = await continuationTask;
            tasks.Add(awaitedComiplerGeneratedTaskOfContinuationTask);
            Task whenall = Task.WhenAll(tasks.ToArray());
            return whenall;
        }
        static async Task FooAsync()
        {
            await Task.Delay(20000);
        }

【讨论】:

  • 干得好追下来。是的,那个错字是一个引人注目的东西。我有一种感觉,这就是为什么我说它是典型的死锁(当你没有SynchronizationContext 时更难遇到)或async“死锁”。不管怎样,很高兴你最后做对了。
【解决方案3】:

您的调度程序可以在被阻塞的线程上安排中间继续。


原文来源:http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

死锁

情况是这样的:请记住,在我的介绍帖子中,在您等待之后 一个任务,当方法继续时,它将在上下文中继续。

在第一种情况下,此上下文是 UI 上下文(适用于任何 UI(控制台应用程序除外)。在第二种情况下,这个上下文是 一个 ASP.NET 请求上下文。

还有一点很重要:ASP.NET 请求上下文不依赖于 特定线程(就像 UI 上下文一样),但它只允许一个 一次穿入。这个有趣的方面不是官方的 记录在任何地方 AFAIK,但在我的 MSDN 文章中提到 关于 SynchronizationContext。

所以这就是发生的事情,从顶级方法开始 (Button1_Click 用于 UI / MyController.Get 用于 ASP.NET):

顶层方法调用 GetJsonAsync(在 UI/ASP.NET 语境)。 GetJsonAsync 通过调用启动 REST 请求 HttpClient.GetStringAsync(仍在上下文中)。获取字符串异步 返回一个未完成的Task,表示REST请求不是 完全的。 GetJsonAsync 等待 GetStringAsync 返回的任务。这 上下文被捕获并将用于继续运行 GetJsonAsync 方法稍后。 GetJsonAsync 返回一个未完成的任务, 表示 GetJsonAsync 方法不完整。顶级 方法同步阻塞 GetJsonAsync 返回的任务。这个 阻塞上下文线程。 … 最终,REST 请求将 完全的。这样就完成了 GetStringAsync 返回的任务。 GetJsonAsync 的延续现在已准备好运行,它正在等待 使上下文可用,以便它可以在上下文中执行。 僵局。顶层方法正在阻塞上下文线程,等待 GetJsonAsync 完成,GetJsonAsync 正在等待 上下文是免费的,因此它可以完成。对于 UI 示例, “上下文”是 UI 上下文;对于 ASP.NET 示例,“上下文”是 ASP.NET 请求上下文。这种类型的死锁可能是由于 要么是“上下文”。

防止死锁

有两种最佳做法(均包含在 我的介绍帖子)可以避免这种情况:

在您的“库”异步方法中,在任何地方使用 ConfigureAwait(false) 可能的。不要阻塞任务;一直使用异步。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多