With C#, the implementation of async can seem to be rather confusing in terms of the proper implementation. To use it in general, it can be quite easy, but without a second thought, there might be asynchronous code that may be written that is not optimal.
A lot of the times however, this creates an insidious problem. The performance penalty of writing it “properly” versus not may be negligible as an optimization, but in terms of reinforcing the best practices, it is rather important. The more it seems that I learn about async, the more I know I am doing it wrong, and there is unfortunately no turning back. Asynchronous ignorance is bliss, until it calls your bluff by deadlocking; followed by ensuring that you will call back by not allowing any other calls.
The keywords “async” and “await” are used very frequently within both C# asynchronous programming, as well as with other very similar implementations in other languages (other names in other languages include promises and futures). The keyword “async” in this context marks a method as being able to use “await” expressions.
Await refers to a method call that happens within an asynchronous method, and it essentially signals to suspend the evaluation of the rest of that asynchronous method until it completes. However, await is non-blocking, meaning that other code is allowed to run, but it blocks the current enclosing method from running until a result is returned.
Async methods require a return type of either a Task, a void, a Task<T> , or a task-like object (i.e., a ValueTask). Tasks and async void methods do not have a return type, but they do differ significantly in their application.
The reason why one would want to use a typeless Task return as opposed to an asynchronous void boils down to the application. As there is explicitly no return type with a void, it cannot be awaited, and this can be an issue. While a Task with no type cannot return a value explicitly, await is important in this context because it allows for the enclosing method to be suspended.
private static async Task DiscordErrorHandler(SocketMessage m, ErrorReason r)
{
await OutputError(m.Channel, r);
await m.DeleteAsync();
}
An example method- to demonstrate the same underlying purpose of a void function; to run code with zero return type.
You can return a Task that resembles completion explicitly within a Task type method, but this is also done implicitly upon running a method with a typeless Task return type. Essentially, a Task type allows for the checking of whether the task correctly executed, while asynchronous void functions do not.
Asynchronous void functions, both based on general guidance, as well as official documentation pertaining to the usage of async/await in C# states that generally, the only use case that async void should be used is in the context of events. Event handlers are methods that are called upon reception of an applicable event, and execute code that has no return type, and therefore, async void functions are the only option available in many situations for asynchronous event handling.
This is also important due to the fact that asynchronous void methods execute code without any direct indication of completion, which can make it difficult to work with as it pertains to debugging (can cause issues if the specific context does not expect the void method to be async). An additional problem pertaining to debugging is that exceptions are also are not able to be caught via the calling method, which can be very difficult. The specific properties in this instance however can be desirable if calling awaited (suspension of the encompassing method until completion) methods from a context that is properly handled.
This is compounded by if the method caller is not aware that it is asynchronous, and it will result in the next method that may be contingent on the operations of the asynchronous void method to call prematurely, as it was called under the assumption that the method already completed, but async voids lack an indicator for this.
As a side note, there is a way to technically have asynchronous void functions notify you when upon completion in the form of a custom implementation of the SynchronizationContext class in your program. As the common English aphorism states, “just because you can, doesn’t mean you should”; and this applies very heavily here. Do not bring out a sniper rifle to kill a mosquito unless you want to spend your time very frustrated.
Async voids also have a side effect of requiring exceptions to be handled within the method explicitly, while Task return type methods do not. This is because of the exception being handled within the Task itself, and therefore, is going to have an exceptional result, as opposed to an exception being thrown. The garbage collector in this context will see the exceptional result and will run the handler TaskScheduler.UnobservedTaskException; which is highly undesirable. In the context of event handlers, async voids have the same behavior as normal void synchronous event handlers, so they are additionally desirable as an ideal solution to asynchronously handling events due to their unique behavior as top-level asynchronous operations.
TL;DR: Basically, use typeless Tasks whenever you can, be careful about error handling before it snowballs, use async voids sparingly and probably only in contexts where you would want to or can run it and forget it. Practically, the only application for this is with event handlers.
public static void Main(string[] args) {
Timer timer = new() {
Enabled = true
};
timer.Elapsed += OnTimedEvent;
}
private static async void OnTimedEvent(object source, ElapsedEventArgs e) {
//Code here
}
One genuinely common use-case for an event handler and having an asynchronous void delegate method.
One maybe obvious, albeit risky solution to run an asynchronous method in a synchronous context is to use Task.Result or Task.Wait. This, without careful consideration, can cause program deadlocking. The best way to typically prevent this is to use these at the enclosing function or task level only. Mixing for example, synchronous calls of asynchronous functions in an asynchronous context is a recipe for disaster. This is firstly bad design, as it defeats the purpose of asynchronous programming by running it synchronously (duh).
The biggest problem is that .Result and .Wait block the calling thread, and if this is done asynchronously, the main thread will not be blocked, but the thread pool threads will be. Eventually, as there is a maximum number of threads allocated to the thread pool that will be reached in the case of many parallel calls, and this will result in enough thread problems to make even the most amateur of knitters feel better about themselves. The only remedy at this point is a program restart, which is obviously a worst-case scenario.
public static void Main(string[] args) {
Task task = Task.Delay(1000);
task.Result;
task.GetAwaiter().GetResult();
}
Two different ways to do effectively the same thing; run a Task synchronously
One solution that is not the most ideal but is still better is using Task.Run() to spawn it onto another thread (on a separate core, but that will be touched on in the next section). While yes, this is not the most advisable workaround when there is no way to do asynchronous all the way downwards in select circumstances, this is one of the few decent ways of doing this without running blocking code; because having your code work is still better than having it deadlock and not work.
This is a common pitfall that I (even in my limited finite wisdom) have gotten myself into. The async keyword is not just a fancy indicator, and in fact has performance implications that while at most times is not a huge deal, it can be- which is why you should employ the best practices. This is even if it requires some greater understanding of async’s implementation in C# more, which is understandably revolting to most if not all. It creates a state machine with methods, and this introduces complexity that if not needed, should be not imposed. The “async” keyword is nothing more than syntactic sugar, and the ending is a little bittersweet.
The keyword does have a time and place, and that is when there is interfacing with input/output (i.e., in a database setting, a calling a webservice). This is because async in this context has the purpose of allowing for the main thread to remain unblocked while waiting for data. Task.Run() allows for it to be offloaded to a different core, which is unnecessary and a waste of resources, when you consider that the CPU is not the limiting factor, but the network or storage is. Therefore, there is a benefit of async I/O calls performing on a background thread to keep the main program responsive, and the main thread free.
With CPU-bound work however, it is advised to have a non-async Task/Task<T> type, and await it within Task.Run(); which is a method that tells the thread pool to queue work. With expensive computations that you would like to run in the background, Task.Run() has the highest performance benefit. This is due to it allowing for the process to run on a separate core, not just a separate thread. CPUs try to complete this work as fast as possible, and this is limited by simply speed of computation rather than storage or a network bottleneck.
Having it run on a separate core allows for when the main thread keeps a core busy for the program to remain responsive, and to partition hardware resources to the computation, as opposed to software. Threads are a figment of a software representation of resources based on the number of cores in your system, but Task.Run() offers the ability to explicitly (when cores are available) to segregate expensive processes to a hardware level.
Also, do not ever commit this sin. If you have an async method with no await calls, this is an absolute waste. The state machine created by inclusion of the async keyword does in fact have actual overhead in terms of memory utilization, so its unnecessary inclusion is simply an unnecessary waste of resources. The warning given in your IDE is for good reason, and for CPU-bound workloads, using Task.Run() and awaiting that Task is a much better use of resources than to arbitrarily add overhead.
The question might be posed: “What is a ValueTask?”. Well, it’s a Task-type object that returns a value; a pretty self-descriptive term- right? Well, that does not describe necessarily the actual reason of why anyone would rather use one over a standard Task<T>.
The answer is, well, usually, you should just stick to Task<T>. You can even convert a ValueTask to one with ValueTask.AsTask if you so need to.
There are limitations with them. You can only await them one time. Generally, never await them more than once, never use Task.Result more than once and/or when it has not completed yet (basically a rule with async in general). Never call ValueTask.AsTask more than once either. If that was not enough stipulations for your tastes, well here is one more! Do not ever mix these together either. The reasoning behind this is that this consumes the task instance in a way; whether synchronously, converting it into a Task<T>, or with awaiting the call. Otherwise, there is a pretty good chance that may be 100% of the time it will have an undefined result.
Then at this point, another question may be raised? Why? Good point, but there is an edge case for where they are useful. The first rule with them is that there is a high likelihood of the result being available synchronously, and the second rule is that there is going to be such a high volume of invocations, that the cost of allocations would in fact become a legitimate issue everytime it would for a Task<T>.
Now, how common would this even be something of consideration? I legitimately do not know. Also, because it contains multiple fields compared to the typed Task’s counterpart of just one field, if you were to implement this to await the ValueTask in an async method, then it will have even MORE overhead than a Task<T>, as now it has to store a struct of multiple fields in the asynchronous state machine compared to just storing a singular instance.
So when will you implement them? I have no idea. I am not you. However, what I can say with better certainty, is that a library you will use may have them at some point, so at least knowing how to deal with them optimally is better than making assumptions that you shouldn’t, because this makes standard asynchronous protocols look way less just arbitrary sounding in comparison.
I hate to be political, but what is a state machine? Well, let us go through not necessarily the logic too, too much- but the implications behind a state machine in particular.
//Here, C# uses interface type generics
private struct BidoofStateMachine : IAsyncStateMachine
{
public State State;
public AsyncTaskMethodBuilder<decimal> Builder;
public string Key;
public StateMachineCompiler RelatedTo;
private TaskAwaiter _taskAwaiter;
//State Management Code
void IAsyncStateMachine.MoveNext()
{
//Iterator Code
}
}
enum State { //Empty for brevity }
This is an example implementation of how an example state machine is generated. Basically, in order to control the flow of a program asynchronously, it has to wrap it into an object basically. A state machine essentially functions as a means to change its state to a different one based on inputs. This makes sense, as a method by itself does not contain information to choose whether to for example, to automatically start itself when awaited after another awaited call.
It requires some logical overhead to do so, and this how C# chooses to abstract it. That is where my bad pun about syntactic sugar at the beginning comes full circle. Pretty clever, right?
Anyways, the reason why this ends up being bitter sweet is because, well- every single time you await a call, it will add this overhead. When you treat it synchronously, it still has that overhead (see why I mentioned ValueTasks before?), and using them haphazardly is something that can lead to performance penalties. Probably not tangibly- but still not only bad practice nonetheless, but also, it can lead to debugging nightmares. As I said before, asynchronous methods barring voids are disadvantaged pertaining to your ability to debug them as clearly, and this is something that unless you benefit from asynchronous calls, do not use them.
public class ExampleClass {
public static async Task Main(string[] args) {
//Main method code
//It will warn you if you have no await calls
//For good reason, I may add
}
}
This is an example implementation of having an asychronous main method, and yes, the main method will be turned into a state machine too…
This leads also into the idea of asynchronous zombification. This may sound like some D-list plot where callback hellspawn come to destroy promise and promise related accessories- but I assure you, it is a real problem. It simply comes from the fact that everytime you call an async method in another method, you have the option of running it synchronously or awaiting the call. You can only await that call given that you have an async method. So in essence, if you have a method that only runs synchronous calls, and you have to run one asynchronous call potentially- that would make that asynchronous, and you will have to incur the same state machine overhead.
This is a problem that is both unavoidable in some contexts unfortunately, but let me speak of another reason to truly rethink how one structures their code. Constructors have no ability to await anything. Sure, one could just run a async void method instead, but this is not just convention- this is convention for a good reason. Do not break it just to be lazy. If not for you, do it for me. Please. So essentially, if you have to execute a method in the constructor that has to be asynchronous, and unless there is a very, very good reason for it to be asynchronous for the first place, now you have blocking code (bad) with state machine overhead that was unnecessary (also bad).
The solution is to just be cognizant of how one structures their code. Sometimes, there is no avoiding the asynchronous zombie, but the key is to minimize the amount of unnecessary bloat that comes with having unnecessary state machines accompanying the method call.
public static async Task<string> PerformAsync() {
return await Task.Run(() => "example");
}
public static async Task RunAsyncCode() {
Console.WriteLine(await ZombieVictim());
}
public static async Task<string> ZombieVictim() {
return (await PerformAsync()) + " Zombie";
}
A rudimentary example, but illustrates how not paying attention to structure can cause the problem to become worse.
Now, just changing the structure a little bit can reduce the amount of state machines, and therefore, the overhead.
public static async Task<string> PerformAsync() {
return await Task.Run(() => "example");
}
public static async Task RunAsyncCode() {
string result = await PerformAsync();
Console.WriteLine(ZombieCure(result));
}
public static string ZombieCure(string result) {
return result + " Cured";
}
Refactored, different but probably better in most contexts, good to keep in mind.
As you can see, consolidating the async- or to keep on with the metaphor, containing the infection (async) is important to make code that makes sense to be async. If you can refactor your code to simply do the asynchronous calls somewhere else (i.e. just do the async method calls in the parent call itself and then pass it to a now synchronous method).
The idea is to make sure that asynchronous methods actually need to execute async, while synchronous methods should do operations that should operate synchronously. It isn’t a no brainer, or otherwise, the zombie wouldn’t be such a problem. Trust me.