Peter Morris
Peter Morris

Reputation: 23224

How can I get Task.ContinueWith to execute async methods in sequence?

Given the following code, I would expect the actions to be executed sequentially

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp32
{
    class Program
    {
        static SpinLock Lock = new SpinLock();
        static Task Head = Task.CompletedTask;

        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            CreateTask(1);
            CreateTask(2);
            CreateTask(3);
            CreateTask(4);
            await Task.Delay(1000); // Half way through executing
            CreateTask(5);
            CreateTask(6);
            CreateTask(7);
            CreateTask(8);
            await Task.Delay(5000);
        }

        static void CreateTask(int i)
        {
            bool lockTaken = false;
            while (!lockTaken)
                Lock.Enter(ref lockTaken);
            try
            {
                Head = Head.ContinueWith(_ => DoActionAsync(i));
            }
            finally
            {
                Lock.Exit();
            }
        }

        static async Task DoActionAsync(int i)
        {
            Console.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss") + ": Creating " + i);
            await Task.Delay(1000);
            Console.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss") + ": Finished " + i);
        }
    }
}

But the actual output is

Hello World!
09:08:06: Creating 1
09:08:06: Creating 2
09:08:06: Creating 3
09:08:06: Creating 4
09:08:07: Finished 2
09:08:07: Finished 3
09:08:07: Finished 4
09:08:07: Creating 5
09:08:07: Finished 1
09:08:07: Creating 6
09:08:07: Creating 7
09:08:07: Creating 8
09:08:08: Finished 7
09:08:08: Finished 6
09:08:08: Finished 8
09:08:08: Finished 5

Why is 2 finishing before 1 rather than not starting until after 1 has completed? (I expect this is the same cause as them all finishing at the same time).

Upvotes: 0

Views: 1283

Answers (2)

Jan-Willem Spuij
Jan-Willem Spuij

Reputation: 46

That is because the tasks are nested. You are creating tasks within tasks without unwrapping them.

try this:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp32
{
    class Program
    {
        static SpinLock Lock = new SpinLock();
        static Task Head = Task.CompletedTask;

        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            CreateTask(1);
            CreateTask(2);
            CreateTask(3);
            CreateTask(4);
            await Task.Delay(1000); // Half way through executing
            CreateTask(5);
            CreateTask(6);
            CreateTask(7);
            CreateTask(8);
            await Task.Delay(5000);
        }

        static void CreateTask(int i)
        {
            bool lockTaken = false;
            while (!lockTaken)
                Lock.Enter(ref lockTaken);
            try
            {
                Head = Head.ContinueWith(_ => DoActionAsync(i)).Unwrap();
            }
            finally
            {
                Lock.Exit();
            }
        }

        static async Task DoActionAsync(int i)
        {
            Console.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss") + ": Creating " + i);
            await Task.Delay(1000);
            Console.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss") + ": Finished " + i);
        }
    }
}

note the Unwrap() The overload you are "abusing" is the general Func<object, T> one where you assign Task to type T instead of a result type. Normally ContinueWith uses a synchronous delegate. .NET thought about this and they have created Unwrap(), which unwraps the nested task.

Upvotes: 3

JonasH
JonasH

Reputation: 36341

.ContinueWith(_ => DoActionAsync(i)) Returns a Task (or specifically a Task<Task>) that completes when DoActionAsync returns. DoActionAsync will return at the first await, thus completing the outer Task and allowing the next operation to begin.

So it will run each "create" serially, but "Finished" will run asynchronously.

A simple solution would be to schedule all the tasks using a LimitedconcurrencyTaskScheduler instead. Or just create a queue of the operations and have a thread processing the queue in order.

Upvotes: 2

Related Questions