0

I got some hard time figuring out how to "store" and call async lambdas in C#, it was basically trying by running. I find out some approach that work while others do not (below), I read about it but it it still unclear why some work and others not, can someone explain why and maybe add a better approach as well?

public static class Program
{
    static async Task Main(string[] args)
    {
        // works fine, everything is printed
        Console.WriteLine("Before 1 delay");
        await Task.Delay(1000);
        Console.WriteLine("After 1 delay");

        // works fine, everything is printed
        var foo = new Func<Task>(async () =>
        {
            Console.WriteLine("Before 2 delay");
            await Task.Delay(1000);
            Console.WriteLine("After 2 delay");
        });
        await foo.Invoke();

        // works fine, everything is printed
        var foo2 = new Action(() =>
        {
            Console.WriteLine("Before 3 delay");
            Task.Delay(1000).Wait();
            Console.WriteLine("After 3 delay");
        });
        foo2.Invoke();

        // Does not work, just before is printed
        var foo3 = new Action(async () =>
        {
            Console.WriteLine("Before 4 delay");
            await Task.Delay(1000);
            Console.WriteLine("After 4 delay");
        });
        foo3.Invoke();
    }
}

Please notice that question is not the same as the following below, even when the answer is the same the context is different, as this question problem is basically why an Action delegate does run async even with the await being there, besides of that there is the "bonus" of "Why .Await() works, while await do not in a action delegate"

async Task vs async void “await” doesn't wait for the completion of call Why is the console window closing immediately once displayed my output? How to stop C# console applications from closing automatically?

Andre
  • 652
  • 2
  • 7
  • 23
  • I'm curious why you would assign `Func`/`Action` to a variable in the first place instead of using them inline and let type inference do its job. At your current state, you might as well define those as static functions on the class or use local functions. – Maximilian Burszley May 25 '21 at 14:41
  • 2
    after `foo3.Invoke();` place a "Console.ReadKey()" -> see what happens. – Fildor May 25 '21 at 14:43
  • Actually my Ide complains saying that it is not possible to infere, I'm not sure if inference is suppose to work here in the case of foo for example... – Andre May 25 '21 at 14:45
  • 1
    @Andre: [Here you go](https://blog.stephencleary.com/2014/02/synchronous-and-asynchronous-delegate.html). – Stephen Cleary May 26 '21 at 00:07

2 Answers2

2

The problem in your last approach is that you're wrapping an async lambda with an Action, which describes a void-returning function.

When await Task.Delay(1000); executes, it tells the runtime to schedule the continuation (i.e. the instructions after that line) after the delay completes asynchronously.

The caller of the delegate at that point has no way of awaiting the inner-async function and its delay, so here we're in the context of an async void invocation, where the operation will eventually complete but no Task-like instance is returned. Since the caller is unable to wait for the function to terminate, it continues its execution until the Main method exits.

In general, async void should only be used for asynchronous event handlers, because as you saw doesn't allow the caller to properly await the function to complete.

Right way to create and call lambda of async methods in C#

// return a Task!
var wrapper = new Func<Task>(async () =>
{
    await SomethingAsync();
});
await wrapper.Invoke();
Tommaso Bertoni
  • 2,333
  • 1
  • 22
  • 22
0
// Does not work, just before is printed
// Action-Delegate => "void" does not return the Task, cannot be awaited.
var foo3 = new Action(async () =>
{
    Console.WriteLine("Before 4 delay");
    await Task.Delay(1000); // Meanwhile, your Program ends!
    Console.WriteLine("After 4 delay");
});
foo3.Invoke(); // <- Starts the action "fire&forget"
//Your Program ends BEFORE second WriteLine can be executed.

^^ see comments in code. If you place anything after that foo3.Invoke(); that prevents your Program from exiting for at least another second, you'll see the "After 4 delay" output.

This is basically an async void Method() - which is a bad idea, except for async event handlers.

Mind that this has other side-effects, too. For example in case of an Exception inside the Action.


var foo2 = new Action(() =>  // _NOT_ async!
{
    Console.WriteLine("Before 3 delay");
    Task.Delay(1000).Wait();  // Blocking!
    Console.WriteLine("After 3 delay");
});
foo2.Invoke(); // Will block. => "works"
Fildor
  • 14,510
  • 4
  • 35
  • 67
  • So somebody doesn't find _both answers_ helpful and doesn't bother to tell us why. Please do, @downvoter: If the answers can be improved, help us do so! – Fildor May 25 '21 at 16:28