3

我正在测试 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。

4

1 回答 1

5

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

确实会发生以下事情:

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

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

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

  • 然后,continuation 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 => await SampleAsyncMethodAsync...效果不佳,因为它会导致包装结果(这里Task<Task>解释得很好)。

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

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 的异步方法。

于 2017-09-15T08:43:07.453 回答