Can anybody explain why this code simply hits a dead end after the WhenAll fires?

Main code:

class AsyncTests
{
    public async void Start()
    {
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - starting whole process, calling await DoWork1()");
        await Task.WhenAll(DoWork1(), DoWork2());
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - finished awaiting DoWork1 and DoWork2");
    }

    public async Task DoWork1()
    {
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - starting DoWork1");
        await DoNothing();
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - finished DoWork1");
    }

    public async Task DoWork2()
    {
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - starting DoWork2");
        await DoNothing();
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - finished DoWork2");
    }

    public Task DoNothing() { return new Task(() => { /* do nothing */ }); }
}

The program control code:

    static void Main(string[] args)
    {            
        var x = new AsyncTests();
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - Main ... calling Start()");
        Task.Run(() => x.Start());
        Console.WriteLine($"Thread:{Thread.CurrentThread.ManagedThreadId} - Main ... start is running");
        Console.ReadKey();
    }

The output:

Thread:1 - Main ... calling Start()
Thread:1 - Main ... start is running
Thread:4 - starting whole process, calling await DoWork1()
Thread:4 - starting DoWork1
Thread:4 - starting DoWork2

UPDATE

To make this a little clearer, let's change it so that DoNothing actually calls Thread.Sleep(2000) and my objective is to run two thread sleeps simultaneously and want to use the async/await pattern to achieve this.

If I change "DoNothing" to be an async Task which performs a sleep, then I get told I need await operators in there. Which means I'd need to write yet another async method to be awaited. So what is the best way to end that chain of calls in terms of operators?

Can somebody show a best practise example of how to achieve the above?

3 Answers

4
Lesiak On Best Solutions

You never start your task in DoNothing.

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=netframework-4.8#remarks

Separating task creation and execution

The Task class also provides constructors that initialize the task but that do not schedule it for execution. For performance reasons, the Task.Run or TaskFactory.StartNew method is the preferred mechanism for creating and scheduling computational tasks, but for scenarios where creation and scheduling must be separated, you can use the constructors and then call the Task.Start method to schedule the task for execution at a later time.

0
John Wu On

Instead of creating a task to return, let the language do it for you.

public async Task DoNothing() { }

The above actually does nothing and will return a Task that is in its completed state and can be awaited.

The way you are currently doing it, the Task is created but it is never started or set to Completed, so awaiting it will lock up the program forever.

0
James Harcourt On

Just to supplement the answers and comments already given, I wanted to show a code example working the way I intended in the test. It shows the execution flow + for info, the managed thread Id of execution at specific times.

The main code:

class AsyncTests
{
    public async Task StartAsync()
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - starting whole process, calling await DoWork1Async()");
        await Task.WhenAll(DoWork1Async(), DoWork2Async());
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - finished awaiting DoWork1Async and DoWork2Async");
    }

    public async Task DoWork1Async()
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - starting DoWork1Async");
        await Sleep();
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - finished DoWork1Async");
    }

    public async Task DoWork2Async()
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - starting DoWork2Async");
        await Sleep();
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Thread:{Thread.CurrentThread.ManagedThreadId} - finished DoWork2Async");
    }        

    public async Task Sleep()
    {
        await Task.Delay(2000);
    }
}

The calling code (note that to make this run asynchronously I had to leave out the await operator which raises the warning consider applying the 'await' operator on the StartAsync() method call.

static void Main(string[] args)
{            
    var x = new AsyncTests();
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - {Thread.CurrentThread.ManagedThreadId} - Main ... calling Start()");
    x.StartAsync();
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - {Thread.CurrentThread.ManagedThreadId} - Main ... start is running");
    Console.ReadKey();
}

Finally, the output - which is as expected showing the code execution/control returning to the places expected for a truly asynchronous operation. As expected two different pool threads were used to run the sleep.

10:43:36.515 - 1 - Main ... calling Start()
10:43:36.546 - Thread:1 - starting whole process, calling await DoWork1Async()
10:43:36.547 - Thread:1 - starting DoWork1Async
10:43:36.561 - Thread:1 - starting DoWork2Async
10:43:36.562 - 1 - Main ... start is running
10:43:38.581 - Thread:4 - finished DoWork2Async
10:43:38.582 - Thread:5 - finished DoWork1Async
10:43:38.582 - Thread:5 - finished awaiting DoWork1Async and DoWork2Async