Search code examples
c#multithreadingtask

What happens when multiple C# tasks start when another task is completed?


Let's say that I have this code:

public async void Run()
{
    TaskCompletionSource t = new TaskCompletionSource();
    Prepare(t.Task);
    await Task.Delay(1000);

    t.SetResult();
    Console.WriteLine("End");
}

public async void Prepare(Task task)
{
    await Run(task, "A");
    await Run(task, "B");
    await Run(task, "C");
    await Run(task, "D");
}

public async Task Run(Task requisite, string text)
{
    await requisite;
    Console.WriteLine(text);
}

What happens when the t.SetResult(); is called? Is this multithread? There is any guarantee of the order of the items in the Console? If I have a List<>, that the Run method changes it, do I need to worry about multithread?


Solution

  • While you wrote a simple await statement;

    await requisite;
    

    The C# compiler moved your method to a separate class and translated that "simple" await into something equivalent to;

    private Task requisite;
    private int state = 0;
    private void MoveNext()
    {
        switch (state)
        {
            case 0:
                var awaiter = requisite.GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    state = 1;
                    awaiter.OnCompleted(MoveNext);
                    return;
                }
                goto case 1;
            case 1:
                // resume execution here when the task is complete
                break;
        }
    }
    

    As you can see, if the task was already complete, your code would continue synchronously. If the task was incomplete, a callback would be registered, to continue execution later.

    The exact implementation of each awaiter can be quite different. For example Task.Yield will return an awaiter that is never complete. Forcing the continuation to execute on the thread pool.

    Most task types, and all the tasks from your example, will call each continuation method synchronously while .SetResult is called.

    Back to your specific example;

    public async void Prepare(Task task)
    {
        await Run(task, "A");
        await Run(task, "B");
        await Run(task, "C");
        await Run(task, "D");
    }
    

    On the first call to Run the task is incomplete, so Run will register a continuation Action and return an incomplete task. The first await in the Prepare method will also see that the returned task is incomplete. Though the method is void so another incomplete task is not returned.

    When you resume after your delay and call t.SetResult();, first the registered continuation to resume the Run method will be called. When the Run method completes, rather than returning, .SetResult will be called on the incomplete task, causing the continuation for Prepare to be called immediately. All of this will occur in the same thread, before t.SetResult returns.

    If you were to place a breakpoint in either Run or Prepare and examine the call stack, you would see how the thread stack is inverted from what you would normally expect. Each returning async function will be there, with a call to SetResult in the stack.