C# Tips & Tricks - FullstackCodingGuy/Developer-Fundamentals GitHub Wiki
Async Programming
Task vs Thread
Threads execute Tasks which are scheduled by a TaskScheduler.
Task represents some work that needs to be done. A Task may or may not be completed. The moment when it completes can be right now or in the future.
Tasks have nothing to do with Threads and this is the cause of many misconceptions, Task is not thread. Task does not guarantee parallel execution. Task does not belong to a Thread or anything like that. They are two separate concepts and should be treated as such.
If the Task is completed and not faulted then the continuation task will be scheduled. Faulted state means that there was an exception. Tasks have an associated TaskScheduler which is used to schedule a continuation Task, or any other child Tasks that are required by the current Task.
Threads just as in any OS represent execution of code. Threads keep track what you execute and where you execute. Threads have a call stack, store local variables, and the address of the currently executing instruction. In C# each thread also has an associated SynchronizationContext which is used to communicate between different types of threads
Task.Yield
When you use async/await, there is no guarantee that the method you call when you do await FooAsync() will actually run asynchronously. The internal implementation is free to return using a completely synchronous path.
If you're making an API where it's critical that you don't block and you run some code asynchronously, and there's a chance that the called method will run synchronously (effectively blocking), using await Task.Yield() will force your method to be asynchronous, and return control at that point. The rest of the code will execute at a later time (at which point, it still may run synchronously) on the current context.
This can also be useful if you make an asynchronous method that requires some "long running" initialization, ie:
private async void button_Click(object sender, EventArgs e) { await Task.Yield(); // Make us async right away
var data = ExecuteFooOnUIThread(); // This will run on the UI thread at some point later
await UseDataAsync(data);
}
Without the Task.Yield() call, the method will execute synchronously all the way up to the first call to await.
Don't Block on Async Code
Avoiding Deadlocks
If you blocked the threads which are supposed to work on the Tasks, then there won’t be a thread to complete a Task.
Examples: Proper ways
The code example should be obvious what it does if you are at least a bit familiar with async/await. The request is done asynchronously and the thread is free to work on other tasks while the server responds. This is the ideal case.
Another way to perform
Everything you do with async and await end up in an execution queue. Each Task is queued up using a TaskScheduler which can do anything it wants with your Task. This is where things get interesting, the TaskScheduler depends on context you are currently in.
Examples: Bad Ways
This type of code should be avoided, they should never be used in libraries that can be called from different contexts.
The code above will also download the string, but it will block the calling Thread while doing so, and it that thread is a threadpool thread, then it will lead to a deadlock if the workload is high enough. Let’s see what it does in more detail:
- Calling HttpClient.GetAsync(url) will create the request, it might run some part of it synchronously, but at some point it reaches the part where it needs to offload the work to the networking API from the OS.
- This is where it will create a Task and return it in an incomplete state, so that you can schedule a continuation.
- But instead you have the Result property, which will blocks the thread until the task completes. This just defeated the whole purpose of async, the thread can no longer work on other tasks, it’s blocked until the request finishes.
This depends on context, so it’s important to avoid writing this type of code in a library where you have no control over the execution context.
If you are calling from UI thread, you will deadlock instantly, as the task is queued for the UI thread which gets blocked when it reaches the Result property. If called from threadpool thread then a theadpool thread is blocked, which will lead to a deadlock if the work load is high enough. If all threads are blocked in the threadpool then there will be nobody to complete the Task. But this case will work if you’re calling from a main or dedicated thread. (which does not belong to threadpool and does not have syncronization context)
Example: Bad way
The code above also blocks the caller, but it dispatches the work to the threadpool. Task.Run forces the execution to happen on the threadpool. So if called from a different thread than a threadpool thread, this is actually pretty okay way to queue work for the threadpool.
- If you have a classic ASP.NET application or a UI application, you can call async functions from sync function using this method, then update the UI based on the result, with the caveat that this blocks the UI or IIS managed thread until the work is done. In case of the IIS thread this is not a huge problem as the request cannot complete until the work is not done, but in case of a UI thread this would make the UI unresponsive.
- If this code is called from a threadpool thread, then again it will lead to a deadlock if the work load is high enough because it’s blocking a threadpool thread which might be necessary for completing the task. Best is to avoid writing code like this, especially in context of library where you have no control over the context your code gets called from.
Well code above is a bit of an exaggeration, just to prove a point. It’s the worst possible thing that you can do. The code above will deadlock no matter what context you are calling from because it schedules tasks for the threadpool and then it blocks the threadpool thread. If called enough times in parallel, it will exhaust the threadpool, and your application will hang… indefinitely. In which case the best thing you can do is a memory dump and restart the application.
What Causes a Deadlock?
Task.Wait() does. That would be the end of story but sometimes it cannot be avoided, and it’s not the only case. Deadlock might also be cause by other sort of blocking code, waiting for semaphore, acquiring as lock. The advice in general is simple. Don’t block in async code. If possible this is the solution. There are many cases where this is not possible to do and that’s where most problems come from.
The problem the developer is facing that the API they are supposed to call is async only, but the function they are implementing is sync. The problem can be avoided altogether by making the method async as well. Problem solved.
But, it turns out that you need to implement a sync interface and you are supposed to implement using API which has async only functions.
- In console applications by default you don’t have a synchronization context, but you have a main thread. Tasks will be queued using the default TaskScheduler and will be executed on the thread pool. You can freely block your main thread it will just stop executing.
- If you create a custom thread, by default you dont have a syncronization context, it’s just like having a console application. Tasks get executed on the thread pool and you can block your custom thread.
- If you are in a thread pool thread, then all following tasks are also executed on the thread pool thread, but if you have blocking code here then the threadpool will run out of threads, and you will deadlock.
- If you are in a desktop UI thread, you have a synchronization context, and by default tasks are queued for execution on the UI thread. Queued tasks are executed one by one. If you block the UI thread there is nothing left to execute tasks and you have a deadlock.
- If you’re writing a dotnet core web application, you’re basically running everything on the thread pool. Any blocking code will block the thread pool and any .Result will lead to a deadlock.
- If you’re writing a ASP.NET web application, then you have theads managed by IIS which will allocate one for each request. Each of these threads has its own syncronization context. Tasks get scheduled for these threads by default. You need to manually schedule for the threadpool for parallel execution. If you call .Result on a task which is enqueued for the request thread, you will instantly deadlock.
- If you’re writing a library, you have no idea what code is calling your code, and mixing async code with sync code, or calling .Result will almost certainly make an application deadlock. Never mix async and sync code in a library.
Good ways of writing async code
- Only call async code only from async code. (dont mix sync with async)
- Never block in async code. (never .Result, never lock)
- If you need a lock, use SemaphoreSlim.WaitAsync()
- Use async/await when dealing with Tasks, instead of ContinueWith/Unwrap, it makes the code cleaner.
- It’s okay to provide both sync and async version of API, but never call one from the other. (this is one of the rare cases when code duplication is acceptable)
Debugging
You have a deadlock in your code? Great! The important part is to identify it. It can be from any Task.Result or Task.Wait or possibly other blocking code. It’s like searching for a needle in a haystack.
Memory Dumps Help a Lot!
If you find your application in a deadlocked state, take a memory dump of your application. Azure has tools for this on portal, if not there are plenty of guides for this. This will capture the state of your application. DebugDiag 2 Analysis can automatically analyze the memory dump. You need to the stack trace on the threads to see where the code is blocked. Upon code review you will find a statement there which blocks the current thread. You need to remove the blocking statement to fix the deadlock.
Reproducing the Deadlock
The other approach is to reproduce the deadlock. The method you can use here is stress testing, launch many threads in parallel and see if the application survives. However this might not be able to reproduce problems, especially if the async tasks complete fast enough. A better approach is to limit the concurrency of the thread pool, when the application starts to 1. This means that if you have any bad async code where a threadpool thread would block then it definitely will block. This second approach of limiting concurrency is also better for performance. Visual Studio is really slow if there are a lot of threads or tasks in you application.
Asynchronous programming scenarios
Asynchronous programming with async and await