Objective
The objective of this document is to identify the common pitfalls and mistakes that occur while implementing, as async/Await is a complex area and even a small mistake or wrong implementation leads to a lot of systems instability issues. The idea is NOT to reinvent the wheel, but instead to capture the best practices and guidelines shared by larger communities.
So in case you find any discrepancy or have a better way to do this, please feel free to reach out to discuss it, and I will update this document as necessary. Moreover it is important for us to get a good hold on the basics and then follow these guidelines with caution. We all as a team need to make sure this document evolves and improves .
Areas
-
Use Async/Await for any services. Don’t blindly use it everywhere
-
Suffix Async function with Async - guideline
-
In the main Task function implementation use Task.FromResult() if any return value or Task.CompletedTask() if no return value.
-
Handling cancellation token. One additional parameter and it should be the last; helpful for canceling any task/request.
-
Instead of using .Result, use GetAwaiter().GetResult. This has a big impact on error handling and aggregated errors. Try to avoid this scenario. Due to Synchronization context, it gets into a dead lock situation.
-
When we don't need the sync context then use ConfigureAwaiter(false)
-
Dont use return from using statement
-
When we use EF then we can’t use in parallel. await.Task.WhenAll or WhenAny. When we use the EF with a database, those are not threadsafe. Use parallelism only when you have threadsafe e.g. external http call or external service; and it should not be db context. If this is a multiple database then it's okay but if we do it on the same database then it is an issue.
Issue # 1 - Async method is called in a void method
InCorrect Usage
- public static async Task FooAsync() {
-
- await Task.Delay(10000);
- }
- public void ThisWillNotWaitForAsyncCodeToComplete() {
- try {
- Console.WriteLine("Before : " + DateTime.Now.ToString());
- FooAsync();
- Console.WriteLine("After : " + DateTime.Now.ToString());
- } catch (Exception ex) {
-
- Console.WriteLine(ex.Message);
- }
- }
Correct Usage
- public static async Task FooAsync() {
- await Task.Delay(10000);
- }
- public async Task ThisWillNotWaitForAsyncCodeToCompleteAsync() {
- Console.WriteLine("Before : " + DateTime.Now.ToString());
- await FooAsync();
- Console.WriteLine("After : " + DateTime.Now.ToString());
- }
Changes to be noted carefully,
-
FooAsync() is called using await.
-
Method having async Task in the signature
Result
-
Async / await chain is followed properly.
-
Parent thread will wait for the child thread to complete. In case of any database or event operations being performed, parent thread will wait for the completion before quitting.
-
SEVERITY Level = FATAL
Issue # 2 - No “async” keyword used and “task” object is returned
InCorrect Usage
- public static Task BarAsync() {
-
- return Task.Delay(10000);
- }
- public void ThisWillNotWaitForAsyncCodeToComplete() {
- try {
- Console.WriteLine("Before : " + DateTime.Now.ToString());
- BarAsync();
- Console.WriteLine("After : " + DateTime.Now.ToString());
- } catch (Exception ex) {
-
- Console.WriteLine(ex.Message);
- }
- }
Correct Usage
- public static async Task BarAsync() {
- await Task.Delay(10000);
- }
- public async Task ThisWillNotWaitForAsyncCodeToCompleteAsyncAsync() {
- Console.WriteLine("Before : " + DateTime.Now.ToString());
- await BarAsync();
- Console.WriteLine("After : " + DateTime.Now.ToString());
- }
Changes to be noted carefully,
-
FooAsync() is using async and not returning task.
-
ThisWillNotWaitForAsyncCodeToCompleteAsync() is calling FooAsync() using await.
Result
-
Async / await chain is followed properly.
-
Parent thread will wait for the child thread to complete. In case of any database or event operations being performed, the parent thread will wait for the completion before quitting.
-
SEVERITY Level = FATAL
Issue # 3 - Method is marked as “async Task” but no async method is called inside.
InCorrect Usage
- Public void Foo() {}
- Public void Bar() {}
- public async Task FakeAsyncMethod() {
- Foo();
- Bar();
- Return Task.CompletedTask;
- }
Correct Usage
- Public void Foo() {}
- Public void Bar() {}
- public void FakeAsyncMethod() {
- Foo();
- Bar();
- }
Changes to be noted carefully,
-
Remove async task from the FakeAsyncMethod()
-
Removed return task statement.
Result
-
Normally the sync method should be called as expected.
-
SEVERITY level = Important
Issue # 4 - async has blocking call instead of using await
InCorrect Usage
- public static async Task FooAsync() {
- await Task.Delay(10000);
- }
- public void ThisWillNotWaitForAsyncCodeToCompleteAsync() {
- Console.WriteLine("Before : " + DateTime.Now.ToString());
- FooAsync().Result;
- Console.WriteLine("After : " + DateTime.Now.ToString());
- }
Correct Usage
- public static async Task FooAsync() {
- await Task.Delay(10000);
- }
- public void ThisWillNotWaitForAsyncCodeToCompleteAsync() {
- Console.WriteLine("Before : " + DateTime.Now.ToString());
- await FooAsync();
- Console.WriteLine("After : " + DateTime.Now.ToString());
- }
Changes to be noted carefully,
-
Added await in FooAsync() to follow the appropriate async/await chain.
-
Using .Result deprives off the async benefit
Result
Issue # 5 - Blocking async method with .Wait
InCorrect Usage
- public async Task FooAsync(string id) {
- …
- some more
- function code without any await operation inside…
- await Task.Delay(10000);
- }
- public void Bar() {
- console.writeline(“Hello world”);
- FooAsync().Wait();
- }
Correct Usage
- public async Task FooAsync(string id) {
- …
- some more
- function code without any await operation inside…
- await Task.Delay(10000);
- }
- public async Task BarAsync() {
- console.writeline(“Hello world”);
- await FooAsync();
- }
Changes to be noted carefully,
-
Use async await in BarAsync() to follow the appropriate async/await pattern.
Result
Issue # 6 - Create task for sync method and wait on the task.
InCorrect Usage
- public void SomeMethod1() {
- …
- some
- function code….
- Var task = Task.Run(() => SomeMethod2);
- task.Wait();…
- some functional code….
- }
- public void SomeMethod2() {
- …
- Some
- function code goes here...
- }
Correct Usage
- public void SomeMethod1() {
- …
- some
- function code….
- SomeMethod2();…
- some functional code….
- }
- public void SomeMethod2() {
- …
- Some
- function code goes here...
- }
Changes to be noted carefully,
-
Use sync method normally the way it is supposed to be used. Making a task and then waiting on the task is just wasting an additional thread on the pool when the same work can be done in the main thread itself.
Result
Issue # 7 - Retrieving result of multiple tasks
InCorrect Usage
- public async Task < string > FooAsync() {
- string result = string.empty;…
- some
- function code…
- return result;
- }
- public async Task < string > BarAsync() {
- string result = string.empty;…
- some
- function code…
- return result;
- }
- public void ParentMethod() {
- var task1 = FooAsync();
- var task2 = BarAsync();
- Task.WaitAll(task1, task2);
- }
Correct Usage
- public async Task < string > FooAsync() {
- string result = string.empty;…
- some
- function code…
- return result;
- }
- public async Task < string > BarAsync() {
- string result = string.empty;…
- some
- function code…
- return result;
- }
- public async Task ParentMethod() {
- var task1 = FooAsync();
- var task2 = BarAsync();
- await task.WhenAll(task1, task2);
- }
Changes to be noted carefully,
We should avoid mixing blocking & unblocking code. Task.WaitAll is a blocking call whereas Task.WhenAll is nonblocking and maintains the async semantics.
Result
-
Code is optimized and works as per the async / await programming guidelines of avoiding blocking calls.
-
SEVERITY level = Important
Issue # 8 - Sync version used when Async is available.
InCorrect Usage
- public bool CheckLabelAlreadyExist(string labelName, Guid facilityKey, int labelTypeCode) {
- return GetQueryable().Any(x => x.DescriptionText == labelName && x.FacilityKey == facilityKey && x.LabelTypeCode == labelTypeCode);
- }
CorrectUsage
- public async Task < bool > CheckLabelAlreadyExist(string labelName, Guid facilityKey, int labelTypeCode) {
- return await GetQueryable().AnyAsync(x => x.DescriptionText == labelName && x.FacilityKey == facilityKey && x.LabelTypeCode == labelTypeCode);
- }
Task Waiting General Rules (phase 1),
To Do This …
|
Instead of This …
|
Use This
|
Retrieve the result of a background task
|
Task.Wait, Task.Result or Task.GetAwaiter.GetResult
|
await
|
Wait for any task to complete
|
Task.WaitAny
|
await Task.WhenAny
|
Retrieve the results of multiple tasks
|
Task.WaitAll
|
await Task.WhenAll
|
Wait a period of time
|
Thread.Sleep
|
await Task.Delay
|
Method Naming Conventions,
-
All async methods should have “async” suffix in the method name for easy readability and differentiation between sync and async methods.
-
Having “async” in the methods, make it more prominent and reduces the chances of error in implementation.
-
This can be done in phase 2
Conclusion
The async/await is the best for IO bound tasks (networking communication, database communication, http request, etc.) but it is not good to apply on computational bound tasks (traverse on the huge list, render a huge image, etc.). Because it will release the holding thread to the thread pool and CPU/cores available will not be involved to process those tasks. Therefore, we should avoid using Async/Await for computational bound tasks.
For dealing with computational bound tasks, I prefer to use Task.Factory.CreateNew with TaskCreationOptions which is LongRunning. It will start a new background thread to process a heavy computational bound task without releasing it back to the thread pool until the task is completed.