-
Notifications
You must be signed in to change notification settings - Fork 0
C# Tips & Tricks
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
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.
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