C# Chained ContinueWith Not Waiting for Previous Task to Complete

半城伤御伤魂 提交于 2020-07-21 03:12:36

问题


I am testing the asynchronousity of C# async/await and came across a surprise where the subsequent code for ContinueWith does not wait for the previous task to complete:

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

Which is executed with the below test method:

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

The output shows execution for runs 0.0,1.0 and 2.0 executes asynchronously correctly but subsequent x.1 and x.2 get started almost immediately and x.2 actually completes before x.1. E.g. as logged below:

[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

It seems the continueWith will only wait on the first task (0) regardless of subsequent chains. I can solve the problem by nesting the second ContinueWith within the first Continuewith block.

Is there something wrong with my code? I'm assuming Console.WriteLine respects FIFO.


回答1:


In short, you expect ContinueWith to wait for a previously returned object. Returning an object (even a Task) in ContinueWith action does nothing with returned value, it does not wait for it to complete, it returns it and passes to the continuation if exists.

The following thing does happen:

  • You run SampleAsyncMethodAsync(0, scopeCount.ToString())
  • When it is completed, you execute the continuation 1:

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

    and when it stumbles upon await Task.Run, it returns a task. I.e., it does not wait for SampleAsyncMethodAsync to complete.

  • Then, continuation 1 is considered to be completed, since it has returned a value (task)
  • Continuation 2 is run.

If you wait for every asynchronous method manually, then it will run consequently:

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

Using ContinueWith(async t => await SampleAsyncMethodAsync... doesn't work as well, since it results into wrapped Task<Task> result (explained well here).

Also, you can do something like:

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

However, it creates some sort of callback hell and looks messy.

You can use await to make this code a little cleaner:

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

Now, it runs 3 tasks for 3 counts, and each task will consequently run asynchronous method with number equal to 1, 2, and 3.



来源:https://stackoverflow.com/questions/46235216/c-sharp-chained-continuewith-not-waiting-for-previous-task-to-complete

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!