Introduction
In the previous post I described the benefits regarding async eliding: performance and allocation (of course by using benchmark as a proof – let us be clear, no benchmark no proof). In this post I will show you that async eliding is not as sweet, great, perfect and lovely and it might seem to be. Let’s get to the point!
HTTP crash
Firstly, I must truly apologise you. The heading is kind of a bait… not 100% though but still. Async eliding can cause the HTTP crash but it is not limited to it – I will just use it as an example. Let’s consider the following example:
public class HttpClientExecutor
{
private readonly string _uri= "https://postman-echo.com/get?foo1=bar1&foo2=bar2";
public async Task<string> GetWithUsingAndWithoutEliding()
{
using var _httpClient = new HttpClient();
return await _httpClient.GetStringAsync(_uri);
}
public Task<string> GetWithUsingAndWithEliding()
{
using var _httpClient = new HttpClient();
return _httpClient.GetStringAsync(_uri);
}
}
The first method, i.e. GetWithUsingAndWithoutEliding
, creates the httpClient
and gets the response from the provided uri. The httpClient
is then disposed. By the way, this is not the best approach of the HttpClient
usage because of the socket exhaustion (probably the best approach now is to use the IHttpClientFactory), but it shows one of the async eliding problems.
Does the second method, i.e. GetWithUsingAndWithEliding
, do the same?
Well yes, but actually no. The HttpClient
is instantiated within the using statement and the GetStringAsync
method is invoked, but then the TaskCanceledException
would be thrown. It happens because the httpClient
is disposed during the HTTP GET request processing.
Exceptions
This time the heading is 100% not a bait. To give some concrete examples let’s consider the following example:
public class ExceptionExecutor
{
public async Task<int> DivideWithoutEliding(int dividend, int divider)
{
var result = dividend / divider;
return await Task.Run(() => result);
}
public Task<int> DivideWithEliding(int dividend, int divider)
{
var result = dividend / divider;
return Task.Run(() => result);
}
}
If you’ve just thought about DivideByZeroException, then you are absolutely right, it will be thrown in a moment, but the point is in what circumstances? To understand the difference please open in a separate tab tests that I’ve prepared for this (I could have pasted it here but it would have produced to much noise).
Let’s focus on the DivideWithoutEliding_DividerIsZeroAndAndAssignTaskToTheVariableFirst_WhenAwaitThrowsDivideByZeroException
and DivideWithEliding_DividerIsZeroAndAssignTaskToTheVariableFirst_WhenAssignToTheVariableThrowsDivideByZeroException
tests. The first one throws the exception NOT when we assign to the task
variable but when we await it. The second one throws the exception when we assign to the task
variable. It happens because when we do the classic async/await approach (not elided) the exception is packed in the returned Task. On the other hand, when we do the async eliding, the exception is thrown before we use the await keyword.
Why would we want to assign Task to the variable and then await it?
We can think of a scenario where we want to run 3 tasks and waits for any of them to complete, in other words we want to use Task.WhenAny. Let’s add two methods to the ExceptionExecutor
class:
public async Task<Task<int>> DivideMany_WithoutEliding(int dividend, int divider)
{
var task1 = DivideWithoutEliding(dividend, divider);
var task2 = DivideWithoutEliding(dividend, divider);
var task3 = DivideWithoutEliding(dividend, divider);
return await Task.WhenAny(result1, result2, result3);
}
public async Task<Task<int>> DivideMany_WithOneEliding(int dividend, int divider)
{
var task1 = DivideWithoutEliding(dividend, divider);
var task2 = DivideWithoutEliding(dividend, divider);
var task3 = DivideWithEliding(dividend, divider);
return await Task.WhenAny(task1, task2, task3);
}
There are two tests that show the behaviour of such a scenario:DivideMany_WithoutEliding_DividerIsZero_WhenAwaitSecondThrowsDivideByZeroException
and DivideMany_WithOneEliding_DividerIsZero_WhenFirstAwaitThrowsDivideByZeroException
.
As always I’ve prepared an example and you can find it on my github, project: async-eliding-part-2, test project: async-eliding-part-2-tests. I encourage you to go through the code, debug it and try to test your own scenarios. Unfortunately, I haven’t prepare any benchmark this time, but I attach great article to read about the async eliding.
Summary
Async eliding is an interesting approach. On the one hand it can improve the performance and the allocation, but on the other hand the program behaviour changes which can cause to the unexpected application errors. Recommended way of using async eliding is not to use it, unless you have a really good reason to boost some of the hot paths` performance of your application and, above all, you are aware of the potential consequences.
Have a nice day, bye!
Be First to Comment