Search code examples
c#.netasync-awaitdrydivide-and-conquer

Async methods chaining with DRY and Divide&Conquer principle


Assume we have the following method:

private async Task<string> Foo(string parameter)
{
    // Some code to convert source parameter
    string convertedParameter = //value;

    CallResult callResult;
    try
    {
        var integerResult = await LoadInformationAsync(convertedParameter);
        if (integerResult > 0)
        {
            callResult = // Some logic to analyse integerResult and generate CallResult object
        }
        else
        {
            callResult = // Some logic to analyse integerResult and generate CallResult object
        }
    }
    catch (Exception ex)
    {
        callResult = CallResult.Default; // some default value if call failed
    }

    var stringResult = // some logic to convert callResult instance to some another string result;

    return stringResult; //Finally return the result
}

Let's not go deep into the details. The main thing is that this method contains some business logic and call (assume 3d party) method LoadInformationAsync that is awaitable.

Behind those comments can be tons of business logic so, I think, everyone will agree that it would be definitely good to split the logic into separate methods(or even classes).

As a result a core-call of LoadInformationAsync method will go deeper into call stack. Something like this:

private async Task<string> Foo(string parameter)
{
    // Some code to convert source parameter
    string convertedParameter = //value;

    CallResult callResult = await MakeSafeCall(convertedParameter);

    var stringResult = // some logic to convert callResult instance to some another string result;

    return stringResult; //Finally return the result
}

private async Task<CallResult> MakeSafeCall(string parameter)
{
    try
    {
        var integerResult = await LoadInformationAsync(convertedParameter);
        if (integerResult > 0)
        {
            return callResult = // Some logic to analyse integerResult and generate CallResult object
        }
        else
        {
            return callResult = // Some logic to analyse integerResult and generate CallResult object
        }
    }
    catch (Exception ex)
    {
        return CallResult.Default;
    }
}

As a result we have slightly better code. F.e. is some class/method might want to call method MakeSafeCall to have there try/catch.

But what do we have now? We have one extra async method that needs to be awaited. And each pair of async/await brrings a state machine capturing the context and so on. Ok, we can deal with this overhead, but what if we have more complicated logic (and very often we really do) that forces us to split our root method into smaller peaces. Our async/await pairs count will increase. And it seems not to be very good.

So the question: what is the good pattern of using async/await in such situations?


Solution

  • I think Stephan Toub has a great answer to your question in his article Async Performance: Understanding the Costs of Async and Await

    Asynchronous methods are a powerful productivity tool, enabling you to more easily write scalable and responsive libraries and applications. It’s important to keep in mind, though, that asynchronicity is not a performance optimization for an individual operation. Taking a synchronous operation and making it asynchronous will invariably degrade the performance of that one operation, as it still needs to accomplish everything that the synchronous operation did, but now with additional constraints and considerations. A reason you care about asynchronicity, then, is performance in the aggregate: how your overall system performs when you write everything asynchronously, such that you can overlap I/O and achieve better system utilization by consuming valuable resources only when they’re actually needed for execution. The asynchronous method implementation provided by the .NET Framework is well-optimized, and often ends up providing as good or better performance than well-written asynchronous implementations using existing patterns and volumes more code. Any time you’re planning to develop asynchronous code in the .NET Framework from now on, asynchronous methods should be your tool of choice.

    The bottom line is, don't optimize prematurely. If you benchmark your code and see that an async method is a bottleneck, see what its doing behind the covers and how you can write your code to be more performance oriented. But keep in mind that the .NET framework team had this in mind when implementing async-await, you can see that in the details, such as making the AsyncTaskMethodBuilder a struct and not a class to reduce GC pressure, etc.

    I suggest you read stephans article thoroughly to get a better understanding of the costs/optimizations made by the framework.