0

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).

Peter Morris
  • 20,174
  • 9
  • 81
  • 146
  • Does this answer your question? [Accuracy of Task.Delay](https://stackoverflow.com/questions/31742521/accuracy-of-task-delay) – Johnathan Barclay Oct 13 '20 at 09:16
  • Is there a particular reason you are using `ContinueWith` rather than `await`? – mjwills Oct 13 '20 at 09:28
  • @JohnathanBarclay No, the Task.Delay is just to make it noticeable by humans. ContinueWith is what should be executing in sequence, not the delay. – Peter Morris Oct 13 '20 at 09:30
  • @mjwills I want to fire and forget calls to a network resource that I want to ensure never occur out of sequence nor get executed at the same time. This is a single-threaded app (Blazor WASM). – Peter Morris Oct 13 '20 at 09:33
  • 2
    Does `Head = Head.ContinueWith(async _ => await DoActionAsync(i));` work? – mjwills Oct 13 '20 at 09:34
  • 2
    You're continuations are calling `DoActionAsync` and are completing when that method returns - not when the `Task` that it returns has completed. You've got too many layers of tasks. – Damien_The_Unbeliever Oct 13 '20 at 09:34
  • @Damien_The_Unbeliever Have you tested this hypothesis? – Peter Morris Oct 13 '20 at 09:35
  • @PeterMorris Damien is correct - your output proves it. – mjwills Oct 13 '20 at 09:37
  • Can you talk us through whether https://dotnetfiddle.net/V4GiEs might be an option? Its much simpler in my mind. – mjwills Oct 13 '20 at 09:40
  • @PeterMorris - Remove the `async` from `DoActionAsync` and it will work fine. Damien is correct. – Enigmativity Oct 13 '20 at 09:41
  • @PeterMorris "_ContinueWith is what should be executing in sequence_" But they do. Look at your "Creating" statements; they are all sequential. It's the call to `Task.Delay` inside `DoActionAsync` that's causing the differences with your "Finished" statements. – Johnathan Barclay Oct 13 '20 at 09:42
  • @mjwills I need fire and forget in a guaranteed sequence, where I can add new tasks at any point (whilst executing, or hours later) – Peter Morris Oct 13 '20 at 09:51
  • @JohnathanBarclay I want the next task to start only when the previous one has completely finished (creating + await + finished) – Peter Morris Oct 13 '20 at 09:53
  • 1
    Would creating your own extension to sequence tasks provide the functionality you're looking for? https://dotnetfiddle.net/5EculP (something like this). Though keep in mind that in this example if one of the tasks crashes the whole chain crashes –  Oct 13 '20 at 09:58
  • @Knoop That does exactly what I am trying to achieve, please add it as an answer. I still don't understand my problem though, I thought that is exactly what ContinueWith does. – Peter Morris Oct 13 '20 at 10:06
  • 1
    Yeah did not add it as an answer since it doesn't answer the question asked: why `ContinueWith` behaves as it does (and tbh there is a lot of complexity under the hood that I don't understand). But enjoy the solution and good luck on your project! –  Oct 13 '20 at 10:10

2 Answers2

3

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.

2

.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.

Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
JonasH
  • 28,608
  • 2
  • 10
  • 23