繁体   English   中英

Task.Result/wait(..) 如果等待的任务链具有“解包”任务,则无限期等待,而如果使用“async/await”则成功完成

[英]Task.Result/wait(..) is indefinitely waits if waited on chain of tasks have 'unwrapped' task, whereas successfully completes if 'async/await' is used

环境: 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 -考虑-使用-任务-ConfigureAwait-false- )

编辑

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

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

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;
}

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

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

问题

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

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

关于调试器 + 任务窗口注意事项“调试器 + 任务”窗口不显示任何细节(请注意,它在我的环境中工作不正确(至少我的理解),因为它从不显示计划外的任务并且有目的地显示活动任务)

无限期等待 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;
}

在 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

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

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

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

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

我发现好奇的是,使用这个实现您的IndefinitelyBlockingTask了两次只要TaskWhichWorks (即1秒VS分别〜500毫秒)。 显然,由于Unwrap ,行为发生了变化。

有人用敏锐的眼光可能会发现问题马上,但我个人不使用任务的延续Unwrap多,所以花了一些时间来沉英寸

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

Unwrap平从返回的任务ContinueWith手段,内部任务不再是发射后不管-现在是执行链的一部分,当你将它添加到列表中,外任务( Task.WhenAll(tasks.ToArray()) ) 无法完成,直到内部任务完成)。

使用ContinueWith(async () => { })不会改变上面描述的行为,因为 async 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否则它基本上“ 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;
}

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

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

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

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);
        }

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


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

僵局

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

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

另一个重点:ASP.NET 请求上下文不绑定到特定线程(如 UI 上下文),但它一次只允许一个线程进入。 这个有趣的方面在 AFAIK 的任何地方都没有正式记录,但在我关于 SynchronizationContext 的 MSDN 文章中提到。

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

顶级方法调用 GetJsonAsync(在 UI/ASP.NET 上下文中)。 GetJsonAsync 通过调用 HttpClient.GetStringAsync(仍在上下文中)启动 REST 请求。 GetStringAsync 返回一个未完成的 Task,表示 REST 请求未完成。 GetJsonAsync 等待 GetStringAsync 返回的 Task。 上下文被捕获,稍后将用于继续运行 GetJsonAsync 方法。 GetJsonAsync 返回一个未完成的Task,说明GetJsonAsync 方法未完成。 顶层方法同步阻塞 GetJsonAsync 返回的 Task。 这会阻塞上下文线程。 … 最终,REST 请求将完成。 这完成了由 GetStringAsync 返回的任务。 GetJsonAsync 的延续现在已准备好运行,它等待上下文可用,以便它可以在上下文中执行。 僵局。 顶层方法正在阻塞上下文线程,等待 GetJsonAsync 完成,而 GetJsonAsync 正在等待上下文空闲以便它可以完成。 对于 UI 示例,“上下文”是 UI 上下文; 对于 ASP.NET 示例,“上下文”是 ASP.NET 请求上下文。 任何一种“上下文”都可能导致这种类型的死锁。

防止死锁

有两个最佳实践(都在我的介绍文章中介绍)可以避免这种情况:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM