【问题标题】:C# Chained ContinueWith Not Waiting for Previous Task to CompleteC# Chained ContinueWith 不等待上一个任务完成
【发布时间】:2018-02-24 09:20:09
【问题描述】:

我正在测试 C# async/await 的异步性,并意外发现 ContinueWith 的后续代码不等待前一个任务完成:

public async Task<int> SampleAsyncMethodAsync(int number,string id)
  {
            Console.WriteLine($"Started work for {id}.{number}");
            ConcurrentBag<int> abc = new ConcurrentBag<int>();

            await Task.Run(() => { for (int count = 0; count < 30; count++) { Console.WriteLine($"[{id}] Run: {number}"); abc.Add(count); } });

            Console.WriteLine($"Completed work for {id}.{number}");
            return abc.Sum();
        }

使用以下测试方法执行:

        [Test]
        public void TestAsyncWaitForPreviousTask()
        {
            for (int count = 0; count < 3; count++)
            {
                int scopeCount = count;

                var c = SampleAsyncMethodAsync(0, scopeCount.ToString())
                .ContinueWith((prevTask) =>
                {
                    return SampleAsyncMethodAsync(1, scopeCount.ToString());
                })
                .ContinueWith((prevTask2) =>
                {
                    return SampleAsyncMethodAsync(2, scopeCount.ToString());
                });
            }
        }

输出显示运行 0.0、1.0 和 2.0 的执行正确地异步执行,但随后的 x.1 和 x.2 几乎立即启动,并且 x.2 实际上在 x.1 之前完成。例如。如下所示:

[2] Run: 0
[2] Run: 0
[2] Run: 0
Completed work for 2.0
Started work for 0.1
Started work for 0.2  <-- surprise!
[0] Run: 2
[0] Run: 2
[0] Run: 2
[0] Run: 2
[0] Run: 2

似乎 continueWith 只会等待第一个任务 (0) 而不管后续链。 我可以通过将第二个 ContinueWith 嵌套在第一个 Continuewith 块中来解决问题。

我的代码有问题吗?我假设 Console.WriteLine 尊重 FIFO。

【问题讨论】:

标签: c# asynchronous continuewith


【解决方案1】:

简而言之,您希望ContinueWith 等待先前返回的对象。在ContinueWith 操作中返回一个对象(甚至是Task)对返回值没有任何作用,它不会等待它完成,而是返回它并传递给延续(如果存在)。

确实会发生以下事情:

  • 你跑SampleAsyncMethodAsync(0, scopeCount.ToString())
  • 当它完成后,你执行延续1:

    return SampleAsyncMethodAsync(1, scopeCount.ToString());
    

    当它偶然发现await Task.Run 时,它会返回一个任务。即,它不会等待 SampleAsyncMethodAsync 完成。

  • 然后,继续 1 被认为已完成,因为它返回了一个值(任务)
  • 继续运行 2。

如果您手动等待每个异步方法,那么它将运行:

for (int count = 0; count < 3; count++)
{
    int scopeCount = count;

    var c = SampleAsyncMethodAsync(0, scopeCount.ToString())
    .ContinueWith((prevTask) =>
    {
        SampleAsyncMethodAsync(1, scopeCount.ToString()).Wait();
    })
    .ContinueWith((prevTask2) =>
    {
        SampleAsyncMethodAsync(2, scopeCount.ToString()).Wait();
    });
}    

使用ContinueWith(async t =&gt; await SampleAsyncMethodAsync... 效果不佳,因为它会导致包装Task&lt;Task&gt; 结果(解释得很好here)。

此外,您可以执行以下操作:

for (int count = 0; count < 3; count++)
{
    int scopeCount = count;

    var c = SampleAsyncMethodAsync(0, scopeCount.ToString())
        .ContinueWith((prevTask) =>
        {
            SampleAsyncMethodAsync(1, scopeCount.ToString())
                .ContinueWith((prevTask2) =>
                {
                    SampleAsyncMethodAsync(2, scopeCount.ToString());
                });
        });   
}

但是,它会创建某种回调地狱并且看起来很混乱。

您可以使用await 使这段代码更简洁:

for (int count = 0; count < 3; count++)
{
    int scopeCount = count;

    var d = Task.Run(async () => {
        await SampleAsyncMethodAsync(0, scopeCount.ToString());
        await SampleAsyncMethodAsync(1, scopeCount.ToString());
        await SampleAsyncMethodAsync(2, scopeCount.ToString());
    });
}   

现在,它运行 3 个任务 3 个计数,因此每个任务将运行异步方法,number 等于 1、2 和 3。

【讨论】:

  • 是的,ContinueWith 的行为确实确认了该行为,但 MSDN 声明该方法返回一个新的继续任务。 msdn.microsoft.com/en-us/library/dd270696(v=vs.110).aspx
  • @TropicalFruitz 好的,现在我明白你的意思了,你是对的。 ContinueWith 确实返回了一个全新的任务。看起来它与使用async 有某种关系。使SampleAsyncMethodAsync 不异步确实会使您的代码运行正确。非常有趣,如果我理解它,我会更新我的答案或删除它。
  • @TropicalFruitz 您能否查看更新后的答案。我不能很好地解释它:(
  • 不用担心。我想我明白了。所以看起来 ContinueWith 返回 Task 的 Task ,因此需要 Unwrap 函数才能到达底部任务,否则后续的 ContinueWith 仍应用于第一个 Task 对象。它解释了奇怪的意外行为。感谢您的帮助和链接!
  • 非常感谢,您正在解决我的问题!
猜你喜欢
  • 1970-01-01
  • 2015-08-10
  • 2012-07-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-16
  • 2023-03-10
相关资源
最近更新 更多