2

I'm trying to understand the consequences of two approaches to passing a lambda into an async method that executes the lambda. The below example distills the two approaches. In the first approach, the lambda itself async whereas it isn't in the second approach.

While this is a contrived example, I'm trying to determine if either approach is more "correct", handles corner cases better (e.g. if the lambda throws), has significantly better performance, and so on. Are these approaches, from the caller's perspective, functionally equivalent?

class Program
{
    static async Task Main(string[] args)
    {
        var result1 = await ExecuteFuncAsync(
            async () =>
            {
                var funcResult = await Task.FromResult(false);

                return funcResult;
            });

        var result2 = await ExecuteFuncAsync(
            () =>
            {
                var funcResult = Task.FromResult(false);

                return funcResult;
            });
    }

    private static async Task<bool> ExecuteFuncAsync(Func<Task<bool>> func)
    {
        return await func();
    }
}
Suraj
  • 35,905
  • 47
  • 139
  • 250
  • 4
    [Stephen Cleary's Blog: "Eliding Async and Await"](https://blog.stephencleary.com/2016/12/eliding-async-await.html) – Fildor Jan 13 '23 at 13:54
  • Suraj lambdas are not much more than fancy methods. All the arguments, pros and cons for eliding async await that apply to methods, apply to lambdas as well. You are not going to get any answer that is better and more thorough or contains information that is not included in the linked question. The issue of eliding is important, and asking about it is great, but it has also been asked [dozens and dozens](https://stackoverflow.com/questions/linked/19098143) times before. – Theodor Zoulias Jan 13 '23 at 14:54

1 Answers1

0

It depends on the context. I would argue that there is no major difference between the two (in the provided sample, in general - see "Eliding Async and Await" by Stephen Cleary).

Though there is a third option which should be considered in cases when the wrapped func is a synchronous one:

var result2 = ExecuteFuncAsync(() => Task.Run(() =>
{
    var funcResult = true;
    return funcResult;
}));

Or for some mainly testing/experimentation scenarios:

var result2 = await ExecuteFuncAsync(async () =>
{
    await Task.Yield();
    var funcResult = true;
    return funcResult;
});

The difference being that async function returns control to the caller at the first await but only if awaited task is not completed yet. Consider the following snippet:

Console.WriteLine("First before");
var first = ExecuteFuncAsync(
    async () =>
    {
        var funcResult = await Task.FromResult(false);
        return funcResult;
    });
Console.WriteLine("First before await");
await first;
Console.WriteLine("First after await");
Console.WriteLine();
Console.WriteLine("Second before");
var second = ExecuteFuncAsync(() =>
    {
        var funcResult = Task.FromResult(false);
        return funcResult;
    });
Console.WriteLine("Second before await");
await second;
Console.WriteLine("Second after await");
Console.WriteLine();
Console.WriteLine("Third before");
var third= ExecuteFuncAsync(async () =>
    {
        await Task.Yield();
        return true;
    });
Console.WriteLine("Third before await");
await third;
Console.WriteLine("Third after await");

static async Task<bool> ExecuteFuncAsync(Func<Task<bool>> func)
{
    var t  = func();
    Console.WriteLine("Inner Before await");
    await t;
    Console.WriteLine("Inner After await");
    return true;
}

Which produces the following output:

First before
Inner Before await
Inner After await
First before await
First after await

Second before
Inner Before await
Inner After await
Second before await
Second after await

Third before
Inner Before await
Third before await
Inner After await
Third after await

See that for the fist two cases the outer before and after await happen only after both inner before and after, while third option has "correct" order of output. This can be a bid deal.in some cases if the inner function is a long-running one and caller relies on the asynchronous behaviour.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Thank you for this! If the caller wraps the method call in a try/catch and the lambda throws, will the catch block get hit in both approaches? – Suraj Jan 13 '23 at 14:05
  • 1
    @Suraj yes, if the call is awaited inside the try-catch. – Guru Stron Jan 13 '23 at 14:06
  • Calling `await Task.Yield();` on the UI thread will yield back on the UI thread. So any blocking operation that follows will run on the UI thread, blocking it. If the intention is to switch to the `ThreadPool` imperatively (instead of using the `Task.Run`), check out [this](https://stackoverflow.com/questions/15363413/why-was-switchto-removed-from-async-ctp-release "Why was 'SwitchTo' removed from Async CTP / Release?") question. – Theodor Zoulias Jan 13 '23 at 14:41
  • @TheodorZoulias removed mention of UI. By the way, would not usual `ConfigureAwait(false)` fix it? – Guru Stron Jan 13 '23 at 14:54
  • 1
    You mean `await Task.Yield().ConfigureAwait(false);`? This doesn't compile. The `Task.Yield` returns a specialized awaitable with fixed behavior. – Theodor Zoulias Jan 13 '23 at 14:56
  • The intention of `Task.Yield` is not to switch to the `ThreadPool`. It does so accidentally, when it is used outside of its physical environment (an environment equipped with a `SynchronizationContext`). The intended purpose of the `Task.Yield`, to me at least, is unknown. The dotnet/runtime repository is using it in [a few dozen places](https://github.com/dotnet/runtime/search?q=Task.Yield), all of them related to testing. Not a single public .NET API exists that calls internally this method. – Theodor Zoulias Jan 13 '23 at 15:06
  • @TheodorZoulias I suspect the intended purpose is to be analogous to `DoEvents`. That is to say when running on a UI thread, to have continuation for the rest of the method go at the end of the queue, thus allowing some UI events to run. Of course, in practice it's basically never the correct solution. In most any situation, the solution is to do as you suggested and move the entire long running operation off of the UI thread, not to occasionally let other UI items run here or there. I could only really see it being useful if you have long running *UI exclusive* work to do. – Servy Jan 13 '23 at 15:10
  • @TheodorZoulias I'm not sure if this is the intention, but it's also occasionally useful in development/testing to ensure the method in question most certainly doesn't return a completed task. (I've never needed it for that reason, but I could imagine why someone might.) – Servy Jan 13 '23 at 15:10
  • @Servy personally I found a use for `Task.Yield` [recently](https://stackoverflow.com/questions/74954180/thread-contention-on-a-concurrentdictionary-in-c-sharp/74958744#74958744), for splitting a long-running method in parts, in order to mimic the OS behavior of time-slicing the available cores to the running threads, but with limiting the concurrency below the number of cores. In all my involvement with async/await, this is literally the only scenario that I know, where using the `Task.Yield` in production would be justified. – Theodor Zoulias Jan 13 '23 at 15:22