Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should nested awaitable operations be awaited?

I have been following this question and I understand the reasons behind the popular (albeit as-yet-unaccepted) answer by Peter Duniho. Specifically, I am aware that not awaiting a subsequent long-running operation will block the UI thread:

The second example does not yield during the asynchronous operation. Instead, by getting the value of the content.Result property, you force the current thread to wait until the asynchronous operation has completed.

I've even confirmed this, for my own benefit, like so:

private async void button1_Click(object sender, EventArgs e)
{
    var value1 = await Task.Run(async () =>
        {
            await Task.Delay(5000);
            return "Hello";
        });

    //NOTE: this one is not awaited...
    var value2 = Task.Run(async () =>
        {
            await Task.Delay(5000);
            return value1.Substring(0, 3);
        });

    System.Diagnostics.Debug.Print(value2.Result); //thus, UI freezes here after 5000 ms.
}

But now I'm wondering: do you need to await all "awaitable" operations nested within an outermost awaitable operation? For example, I can do this:

private async void button1_Click(object sender, EventArgs e)
{
    var value0 = await Task.Run(() =>
        {
            var value1 = new Func<Task<string>>(async () =>
            {
                await Task.Delay(5000);
                return "hello";
            }).Invoke();

            var value2 = new Func<string, Task<string>>(async (string x) =>
                {
                    await Task.Delay(5000);
                    return x.Substring(0, 3);
                }).Invoke(value1.Result);

            return value2;
        });

    System.Diagnostics.Debug.Print(value0); 
}

Or I can do this:

private async void button1_Click(object sender, EventArgs e)
{
    //This time the lambda is async...
    var value0 = await Task.Run(async () =>
        {
            //we're awaiting here now...
            var value1 = await new Func<Task<string>>(async () =>
            {
                await Task.Delay(5000);
                return "hello";
            }).Invoke();

            //and we're awaiting here now, too...
            var value2 = await new Func<string, Task<string>>(async (string x) =>
                {
                    await Task.Delay(5000);
                    return x.Substring(0, 3);
                }).Invoke(value1);

            return value2;
        });

    System.Diagnostics.Debug.Print(value0); 
}

And neither of them freeze the UI. Which one is preferable?

like image 971
rory.ap Avatar asked Jan 29 '15 17:01

rory.ap


1 Answers

The last one is preferable (although quite messy)

In TAP (Task-based Asynchronous Pattern) A task (and other awaitables) represent an asynchronous operation. You have basically 3 options of handling these tasks:

  • Wait synchronously (DoAsync().Result, DoAsync().Wait()) - Blocks the calling thread until the task is completed. Makes your application more wasteful, less scalable, less responsive and susceptible to deadlocks.
  • Wait asynchronously (await DoAsync()) - Doesn't block the calling thread. It basically registers the work after the await as a continuation to be executed after the awaited task completes.
  • Don't wait at all (DoAsync()) - Doesn't block the calling thread, but also doesn't wait for the operation to complete. You are unaware of any exceptions thrown while DoAsync is processed

Specifically, I am aware that not awaiting a subsequent long-running operation will block the UI thread

So, Not quite. If you don't wait at all, nothing will block but you can't know when or if the operation completed successfully. However, if you wait synchronously you will block the calling thread and you may have deadlocks if you're blocking the UI thread.

Conclusion: You should await your awaitables as long as that's possible (it isn't in Main for example). That includes "nested async-await operations".

About your specific example: Task.Run is used to offload CPU-bound work to a ThreadPool thread, which doesn't seem to be what you are trying to mimic. If we use Task.Delay to represent a truly asynchronous operation (usually I/O-bound) we can have "nested async-await" without Task.Run:

private async void button1_Click(object sender, EventArgs e)
{
    var response = await SendAsync();
    Debug.WriteLine(response); 
}

async Task<Response> SendAsync()
{
    await SendRequestAsync(new Request());
    var response = await RecieveResponseAsync();
    return response;
}

async Task SendRequestAsync(Request request)
{
    await Task.Delay(1000); // actual I/O operation
}

async Task<Response> RecieveResponseAsync()
{
    await Task.Delay(1000); // actual I/O operation
    return null;
}

You can use anonymous delegates instead of methods, but it's uncomfortable when you need to define the types and invoke them yourself.

If you do need to offload that operation to a ThreadPool thread, just add Task.Run:

private async void button1_Click(object sender, EventArgs e)
{
    var response = await Task.Run(() => SendAsync());
    Debug.WriteLine(response); 
}
like image 197
i3arnon Avatar answered Sep 30 '22 15:09

i3arnon