GeorgeDuke
GeorgeDuke

Reputation: 151

C# async/await await doesn't await

I am using .Net 4.7.2 and C# 7

In a class there are serveral methods, all void. Some of them can be executed asynchronously, others have to be executed sequentially. So I defined a list for the parallels

private void ExecuteSequential()
{
    SomeMethod1();
    SomeMethod2();
    ExecuteParallelAsync();
    SomeMethod3();
    SomeMethod4();
}


private async void ExecuteParallelAsync()
{
    List<Action> methods = new List<Action>()
    {
        MyMethod1,
        MyMethod2,
        MyMethod3
    };


    await Task.Run( () => { Parallel.ForEach( methods , ( currentMethod ) => currentMethod.Invoke() ); } );            
}

Before SomeMethod3 happens to be executed ExecuteParallelAsync should be finished entirely. Because that's not the case, apparently something is wrong with my usage of async and await.

What I am doing wrong?

Thanks in advance!

Upvotes: 0

Views: 2242

Answers (5)

Tohm
Tohm

Reputation: 305

a small complete example:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace test
{
    class Program
    {
        static void Main(string[] args)
        {
            SomeMethod1();
            ExecuteParallelAsync();
            SomeMethod2();
            Console.ReadKey();
        }

        static void SomeMethod1()
        {
            Console.WriteLine("SomeMethod1");
        }
        static void SomeMethod2()
        {
            Console.WriteLine("SomeMethod2");
        }

        static void ExecuteParallelAsync()
        {
            Console.WriteLine("\tstart of ExecuteParallelAsync");
            var rd = new Random();

            List<Action> methods = new List<Action>()
            {
                ()=>{Thread.Sleep((int) Math.Ceiling(rd.NextDouble()*1000));Console.WriteLine("MyMethod1"); },
                ()=>{Thread.Sleep((int) Math.Ceiling(rd.NextDouble()*1000));Console.WriteLine("MyMethod2"); },
                ()=>{Thread.Sleep((int) Math.Ceiling(rd.NextDouble()*1000));Console.WriteLine("MyMethod3"); },
            };
            Task.WaitAll(methods.Select((action) => Task.Run(action)).ToArray());

            Console.WriteLine("\tend of ExecuteParallelAsync");
        }

    }
}

example of a result:

SomeMethod1
        start of ExecuteParallelAsync
MyMethod2
MyMethod3
MyMethod1
        end of ExecuteParallelAsync
SomeMethod2

Upvotes: 0

tmaj
tmaj

Reputation: 34947

Step 1, reproduce

public void Method()
    {
        SomeMethod();
        SomeMethod();
        ExecuteParallelAsync(); //< ---fire and forget 
        SomeMethod();
        SomeMethod();
        
        Console.ReadLine();
    }

    private void SomeMethod() => Console.WriteLine($"SomeMethod");
    
    private async void ExecuteParallelAsync()
    {
        Console.WriteLine($"START");
        await Task.Delay(TimeSpan.FromMilliseconds(100));
        Console.WriteLine($"END");
    }

Output

SomeMethod
SomeMethod
START
SomeMethod
SomeMethod
END

Step 2, trying to fix

"Oh yes, I forgot to await my function"

await ExecuteParallelAsync(); //Change 1. "I forgot to await"

Output

Compilation error (line 12, col 3): The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
Compilation error (line 12, col 3): Cannot await 'void'

Step 3, fix

Addressing both errors from step 2.

// Change 2
// was:public void Main()  
// which caused: "The 'await' operator can only (...)"
public async Task Main() 
(...)
// Change 3
// was: private async void ExecuteParallelAsync()
// which caused "Compilation error (line 12, col 3): Cannot await 'void'"
private async Task ExecuteParallelAsync()

Run.

Output:

SomeMethod
SomeMethod
START
END
SomeMethod
SomeMethod

For reference, we could have waited for ExecuteParallelAsync without the await keyword, but await is the correct way in all scenarios except the tiny minority where it's not.

Upvotes: 0

Gabriel Luci
Gabriel Luci

Reputation: 40858

Marc's answer is the correct solution, but I wanted to explain more about why.

A very key thing to understand about async methods is that:

  1. They start synchronously.
  2. When the await keyword is used, and await given an incomplete Task, the method returns - yes, even if you're only halfway through your method. The magic is in what it returns.

So lets look at what your code does:

  1. Call ExecuteParallelAsync().
  2. ExecuteParallelAsync() starts running synchronously.
  3. Task.Run returns a Task object that is not complete yet.
  4. The await keyword inspects the Task, sees that it is not complete, so it creates a new Task, signs up the rest of the method as a continuation of that Task (there is nothing left to do in this case, but it still happens) and returns.
  5. Because the return type is void it cannot return a Task. But it still returns!

So all that means that ExecuteParallelAsync() returns before the Task.Run is completed, and ExecuteSequential() has no way to tell when the work is actually complete.

This is commonly called "fire and forget" (i.e. "go do this and I don't care when or if you finish").

As Marc's answer points out, if you want to know when it's done, you need to return a Task, which you can then await to know when it's done.

Microsoft has very well written articles about asynchronous programming that you may benefit from reading: Asynchronous programming with async and await

Upvotes: 4

Tohm
Tohm

Reputation: 305

If you want an implementation without async/await sugar keyword:

private void ExecuteParallelAsync()
{
    List<Action> methods = new List<Action>()
    {
        MyMethod1,
        MyMethod2,
        MyMethod3
    };

    Task.WaitAll(methods.Select((action) => Task.Run(action)).ToArray())   
}

Upvotes: 1

Marc Gravell
Marc Gravell

Reputation: 1062520

async void - all bets are off; in reality, you should never write an async void method unless you're in the tiny tiny set of extreme corner cases that require it, such as async event-handlers.

Before SomeMethod3 happens to be executed ExecuteParallelAsync should be finished entirely.

how could it possibly know? it is: async void, which means it doesn't give anything to the caller that indicates the state.

To do that, it must be:

private async Task ExecuteParallelAsync() // or ValueTask

and you must await it at the call site

await ExecuteParallelAsync();

Upvotes: 12

Related Questions