0

I'm familiar with async/await, returning threads to the pool on an await with an async function. I can see the benefit of this in a controller method returning the thread for another API requests, and in any 3rd party API calls, such as AWS.

But is there any benefit in using it throughout the entire call stack? Consider this pseudocode example:

public class MyController
    public async Task<IActionResult> MyFirstFunction()
    {
        var result = await _myHandler.MySecondFunction();
        ...
        return Ok();
    }
}

public class MyHandler
    public Task<bool> MySecondFunction()
    {
        ...
        return thirdPartyHandler.ThirdPartyFunction(object);
    }
}

public class ThirdPartyHandler
    public async Task<bool> ThirdPartyFunction(Object object)
    {
        return await thirdParty.ExternalFunctionAsync(object);
    }
}

Would this code not achieve the same thing as having made the 'MySecondFunction' function async, and awaited the ThirdPartyFunction() function call, with less overhead of having to deal with capturing a context and returning a thread to the pool etc?

Admittedly the MyHandler and ThirdPartyHandler are somewhat redundant and could be combined

  • 4
    https://blog.stephencleary.com/2016/12/eliding-async-await.html – JohanP Apr 20 '20 at 06:30
  • After consideration, I think I have the answer to my own question. Unless you need to access the results of the Task, in the middle layer there is no point in awaiting in the middle layer. But usually in such a circumstance, there could be an architectural problem, where you likely have redundant layers that could be refactored – Marcel Rienks Apr 20 '20 at 07:02
  • Also read https://blog.stephencleary.com/2013/11/there-is-no-thread.html. "I'm familiar with async/await, returning threads to the pool on an await with an async function" - there is no thread. There is nothing about `async`/`await` that specifically does anything with threads. – Enigmativity Apr 20 '20 at 07:26

3 Answers3

2

I'm familiar with async/await, returning threads to the pool on an await with an async function.

That's not quite how async/await works. await just returns to its caller. It doesn't return the thread to the thread pool immediately. In an ASP.NET scenario, the awaits act just like normal returns all the way up to the controller action. When the controller action does an await, it will return to the ASP.NET runtime, which is what actually returns the thread to the thread pool.

Another helpful thing to keep in mind is that this code:

var result = await MyFunctionAsync();

is roughly equivalent to this code:

var task = MyFunctionAsync();
var result = await task;

In other words, the method is first called synchronously. When MyFunctionAsync hits an await and needs to yield, it will return an incomplete task. Then this function awaits that task - and if it needs to yield, it will return an incomplete task to its caller, etc. There's no thread switching going on here - just return values on a normal call stack.

So, using await all the way will not destroy your performance. It is how it is designed to be used.


public Task<bool> MySecondFunction()
{
  ...
  return thirdPartyHandler.ThirdPartyFunction(object);
}

Would this code not achieve the same thing as having made the 'MySecondFunction' function async, and awaited the ThirdPartyFunction() function call, with less overhead of having to deal with capturing a context and returning a thread to the pool etc?

Probably not. Eliding async and await has a number of pitfalls. Unless the ... in that function is truly trivial, then you would want to keep the async and await keywords. The overhead is minimal.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
0

In your example you pass the Task without the await. That is perfectly valid, you do not have to await a task if you can return it.

A common misconception is that await actually fetches a thread from the thread pool to execute the task. It does not. Awaiting a task will simply cause the compiler to create a state machine of your method so that the thread can exit the stack and return to the thread pool. It is the Task being awaited that decides what happens. It could be a fully synchronous task that simply finishes its work on current thread like normal, or it could be another thread, or it could wait for callback.

So performance wise it doesn't really matter much if you return await, let the compiler worry about optimizing that away.

public string Result;
public async Task Something()
{
    Thread.Sleep(1000);
    Result = "Something(): Thread: " + Thread.CurrentThread.ManagedThreadId;
}

void Main()
{
    Result = "Main(): Thread: " + Thread.CurrentThread.ManagedThreadId;
    Something();
    Console.WriteLine("Before: " + Result);
    Thread.Sleep(2000);
    Console.WriteLine("After: " + Result);
}

The result here is:

Before: Something(): Thread: 6
After: Something(): Thread: 6

To get the expected result the method has to pull in a thread to do the work:

await Task.Run(() =>
{
    Thread.Sleep(1000);
    Result = "Something(): Thread: " + Thread.CurrentThread.ManagedThreadId;
});

or

public Task Something()
{
    return Task.Run(() =>
    {
        Thread.Sleep(1000);
        Result = "Something(): Thread: " + Thread.CurrentThread.ManagedThreadId;
    });
}

Both return:

Before: Main(): Thread: 6
After: Something(): Thread: 25
Tedd Hansen
  • 12,074
  • 14
  • 61
  • 97
0

Yes, but not literally.

The real rule is not having sync over async in the call chain.

Besides the considerations made by Stephen Cleary (linked by JohanP) the same uses cases as for iterators apply. You might want to validate your parameters right away:

public Task DoSomethingAsync(string param1, int param2)
{
    // validate parameters and throw if invalid

    return DoSomethingAsyncImpl(param1, param2);

    static async Task DoSomethingAsyncImpl(string param1, int param2)
    {
        // do async stuff
    }
}
Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59