Click here to Skip to main content
15,885,914 members
Please Sign up or sign in to vote.
5.00/5 (1 vote)
See more:
I am playing in a new Console application in an attempt to finally get a proper understanding of Task and its various methods.

Below is my test program and the output from it. My question is: why do I get the 'Ending task' messages after the program reaches the statements after the Task.WhenAll() statement?

public static class Program
{
    private const int NumTasks = 10;
    private static int[] TaskDelays = { 1000, 1200, 1400, 1600, 1800, 2000, 2200, 2400, 2600, 2800 };
    private static List<Task> _tasks = new();

    static async Task Main( string[] args )
    {
        Console.WriteLine( "****** START ******" );

        await Run();

        Console.WriteLine( "****** END ******" );
        Console.WriteLine( "Press any key to end." );
        Console.ReadKey();
    }


    private static async Task Run()
    {
        Console.WriteLine( "Create tasks" );
        for( int i = 0; i < NumTasks; ++i )
            _tasks.Add( CreateNewTask( i ) );

        Console.WriteLine( "Start tasks" );
        Console.WriteLine( TaskStatusString() );
        for( int i = 0; i < NumTasks; ++i )
            _tasks[ i ].Start();

        Console.WriteLine( "Await tasks" );
        Console.WriteLine( TaskStatusString() );
        await Task.WhenAll( _tasks );

        Console.WriteLine( "After awaiting tasks" );
        Console.WriteLine( TaskStatusString() );
    }


    private static Task CreateNewTask( int index ) => new Task( async () =>
    {
        Console.WriteLine( $"Starting task {index}." );
        await Task.Delay( TaskDelays[ index ] );
        Console.WriteLine( $"Ending task {index}." );
    });


    private static string TaskStatusString()
    {
        StringBuilder sb = new StringBuilder( "Statuses: " );
        for( int i = 0; i < NumTasks; ++i )
            sb.Append( i.ToString() + "=" + _tasks[ i ].Status.ToString() + "; " );
        sb.Append( Environment.NewLine );
        return sb.ToString();
    }
}


Quote:
****** START ******
Create tasks
Start tasks
Statuses: 0=Created; 1=Created; 2=Created; 3=Created; 4=Created; 5=Created; 6=Created; 7=Created; 8=Created; 9=Created;

Await tasks
Statuses: 0=Running; 1=Running; 2=Running; 3=Running; 4=Running; 5=WaitingToRun; 6=WaitingToRun; 7=WaitingToRun; 8=WaitingToRun; 9=WaitingToRun;

Starting task 6.
Starting task 5.
Starting task 3.
Starting task 0.
Starting task 1.
Starting task 2.
Starting task 4.
Starting task 7.
Starting task 8.
Starting task 9.
After awaiting tasks
Statuses: 0=RanToCompletion; 1=RanToCompletion; 2=RanToCompletion; 3=RanToCompletion; 4=RanToCompletion; 5=RanToCompletion; 6=RanToCompletion; 7=RanToCompletion; 8=RanToCompletion; 9=RanToCompletion;

****** END ******
Press any key to end.
Ending task 0.
Ending task 1.
Ending task 2.
Ending task 3.
Ending task 4.
Ending task 5.
Ending task 6.
Ending task 7.
Ending task 8.
Ending task 9.


What I have tried:

Not sure what to try but I might take up knitting as an alternative to programming.

Changing the code to this gives the output below, which is exactly what I expect, though I am still not 100% clear on why this new version seems to operate differently to my original version.

    private const int NumTasks = 10;
    private readonly static int[] TaskDelays = { 5000, 6000, 6000, 6000, 8000, 6000, 10000, 12000, 1000, 28000 };
    private static List<Task> _tasks = new();

    static async Task Main( string[] args )
    {
        Debug.WriteLine( "****** START ******" );

        await Run();

        Debug.WriteLine( "****** END ******" );
        Debug.WriteLine( "Press any key to end." );
        Console.ReadKey();
    }


    private static async Task Run()
    {
        for( int i = 0; i < NumTasks; ++i )
            _tasks.Add( ReturnDelayedResultAsync( i ) );

        Debug.WriteLine( "Before awaiting tasks" + TaskStatusString() );
        await Task.WhenAll( _tasks );
        Debug.WriteLine( "After awaiting tasks" + TaskStatusString() );

        List<int> results = new();
        foreach( var task in _tasks )
        {
            int result = ((Task<int>)task).Result;
            results.Add( result );
        }

        for( int i = 0; i < NumTasks; ++i )
            Debug.WriteLine( $"Task[ {i} ] = {results[ i ]}" );
    }


    private static async Task<int> ReturnDelayedResultAsync( int index )
    {
        Debug.WriteLine( $"Starting task {index}." );
        await Task.Delay( TaskDelays[ index ] );
        Debug.WriteLine( $"Ending task {index}." );
        return index * 2;
    }


    private static string TaskStatusString()
    {
        StringBuilder sb = new StringBuilder( ": " );
        for( int i = 0; i < NumTasks; ++i )
            sb.Append( i.ToString() + "=" + _tasks[ i ].Status.ToString() + "; " );
        sb.Append( Environment.NewLine );
        return sb.ToString();
    }
}


Quote:
****** START ******
Starting task 0.
Starting task 1.
Starting task 2.
Starting task 3.
Starting task 4.
Starting task 5.
Starting task 6.
Starting task 7.
Starting task 8.
Starting task 9.
Before awaiting tasks: 0=WaitingForActivation; 1=WaitingForActivation; 2=WaitingForActivation; 3=WaitingForActivation; 4=WaitingForActivation; 5=WaitingForActivation; 6=WaitingForActivation; 7=WaitingForActivation; 8=WaitingForActivation; 9=WaitingForActivation;

Ending task 8.
Ending task 0.
Ending task 1.
Ending task 2.
Ending task 5.
Ending task 3.
Ending task 4.
Ending task 6.
Ending task 7.
Ending task 9.
After awaiting tasks: 0=RanToCompletion; 1=RanToCompletion; 2=RanToCompletion; 3=RanToCompletion; 4=RanToCompletion; 5=RanToCompletion; 6=RanToCompletion; 7=RanToCompletion; 8=RanToCompletion; 9=RanToCompletion;

Task[ 0 ] = 0
Task[ 1 ] = 2
Task[ 2 ] = 4
Task[ 3 ] = 6
Task[ 4 ] = 8
Task[ 5 ] = 10
Task[ 6 ] = 12
Task[ 7 ] = 14
Task[ 8 ] = 16
Task[ 9 ] = 18
****** END ******
Press any key to end.
Posted
Updated 9-Nov-21 1:05am
v3
Comments
Richard MacCutchan 9-Nov-21 4:03am    
Because the tasks run independently the outputs to the console will come in random order.
Patrick Skelton 9-Nov-21 4:07am    
Hi, Richard. I'm not sure I have phrased my question very well. What I don't understand is how any code can be running after the Task.WhenAll(). It seems to be doing nothing.
Richard MacCutchan 9-Nov-21 4:21am    
The code has all finished, but the system still has to send the console messages out to the console, all of which are fairly low priority.

Because you are thinking in a linear way, instead of a multithreaded manner: each thread - each of your tasks and the main thread that started them - is an independant "runnable entity" which will execute on a core as one becomes available as and when the task is ready to run (i.e. not delayed or otherwise waiting).

But ... they all output to a common device: the console which is updated as and when data is available. Since you don't attempt to lock the console, the order in which data appears is pretty much random - though a quick perusal of the Reference Source does show that MS try to be "atomic" in the output by using block transfer instructions to make a "whole message" appear each time rather than "interlacing" outputs from different threads.

Since Windows threading model is preemptive, the order in which any thread execution happens is indeterminate, though if there is little actual code involved it's quite likely that it'll complete in single visit to the task scheduler. Which is probably what's happing with your main thread: it runs to completion before the other threads actually finish buffering data to the Console.

This is complicated, and you can twist your brain in strange ways if you "overthink it"!
 
Share this answer
 
Comments
Patrick Skelton 9-Nov-21 6:36am    
It's strange but when I use 'old fashioned' ways of threading (I worked a while using C++ and RTOS), I had no problems conceptualising all this. I just seem to have a mental block on how await actually works, no matter how much I read about it. I have updated my question to show code that behaves as I expected it to behave but am still struggling to see precisely how it differs fundamentally from my original example. The penny might drop one day. Thank you for the explanation.
Quote:
C#
private static Task CreateNewTask( int index ) => new Task( async () =>
{
    Console.WriteLine( $"Starting task {index}." );
    await Task.Delay( TaskDelays[ index ] );
    Console.WriteLine( $"Ending task {index}." );
});
This is the root of your problem.

The Task constructor you're calling[^] accepts an Action, not a Func<Task>. Your delegate returns after the first await call, at which point the parent Task has no idea that your delegate is still running. The parent Task completes immediately, but your delegate doesn't complete until after the specified delay.

Change your code to use the Task.Run method[^] instead, and remove the loop which starts the tasks. You will then get the expected output in (more-or-less) the right order - eg:
C#
private static Task CreateNewTask( int index ) => Task.Run( async () =>
{
    Console.WriteLine( $"Starting task {index}." );
    await Task.Delay( TaskDelays[ index ] );
    Console.WriteLine( $"Ending task {index}." );
});
C#
Console.WriteLine( "Create tasks" );
for( int i = 0; i < NumTasks; ++i )
    _tasks.Add( CreateNewTask( i ) );

/*
Console.WriteLine( "Start tasks" );
Console.WriteLine( TaskStatusString() );
for( int i = 0; i < NumTasks; ++i )
    // This line would throw an exception since the task has already started:
    _tasks[ i ].Start(); 
*/

Console.WriteLine( "Await tasks" );
Console.WriteLine( TaskStatusString() );
await Task.WhenAll( _tasks );
****** START ******
Create tasks
Await tasks
Starting task 6.
Starting task 4.
Starting task 5.
Starting task 3.
Starting task 7.
Statuses: 0=WaitingForActivation; 1=WaitingForActivation; 2=WaitingForActivation; 3=WaitingForActivation; 4=WaitingForActivation; 5=WaitingForActivation; 6=WaitingForActivation; 7=WaitingForActivation; 8=WaitingForActivation; 9=WaitingForActivation; 

Starting task 8.
Starting task 9.
Starting task 2.
Starting task 0.
Starting task 1.
Ending task 0.
Ending task 1.
Ending task 2.
Ending task 3.
Ending task 4.
Ending task 5.
Ending task 6.
Ending task 7.
Ending task 8.
Ending task 9.
After awaiting tasks
Statuses: 0=RanToCompletion; 1=RanToCompletion; 2=RanToCompletion; 3=RanToCompletion; 4=RanToCompletion; 5=RanToCompletion; 6=RanToCompletion; 7=RanToCompletion; 8=RanToCompletion; 9=RanToCompletion; 

****** END ******


It would probably be a good idea to install the Microsoft.VisualStudio.Threading.Analyzers NuGet package[^], which will add analyzers which would warn you about this pattern:
vs-threading/VSTHRD101.md at main · microsoft/vs-threading · GitHub[^]
 
Share this answer
 
v2
Comments
Patrick Skelton 10-Nov-21 3:46am    
That is a good explanation, Richard. Thank you. I feel like I understand all that you said with the exception that I'm still not sure why the await Tasks.WhenAll() back in the main function doesn't 'seem' to do what I expect it to do. If you forgive the vague terminology, doesn't that, in effect, make the top-level thread aware of the sub-threads?
Richard Deeming 10-Nov-21 3:52am    
You've essentially got three levels:

1) The Main thread;
2) The Tasks started by new Task(...);
3) The Tasks created by your async void delegate.

The main thread waits for the tasks from level 2. But since there's no way to monitor the progress of an async void, the level 2 tasks can't wait for the level 3 tasks. As a result, the main thread completes before the level 3 tasks have finished. If it wasn't for the Console.ReadKey line, your program would exit before the level 3 tasks had completed.

Avoid async void methods | You’ve Been Haacked[^]
Removing async void | John Thiriet[^]
Patrick Skelton 10-Nov-21 3:58am    
Ah, I see! I didn't spot that I was implicitly creating an async void. One step closer to the penny dropping... I hope...
Patrick Skelton 10-Nov-21 3:48am    
PS - Thanks for the tip about the analyser. I don't like what it tells me about my code but that's undoubtedly a good thing.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900