Asynchronous Tasks in .NET and JavaScript
One of the challenges developers face when developing code is understanding what an asynchronous task is and its difference with parallel tasks.
Despite modern framework made creating asynchronous code a task relatively transparent to their users, understanding the concept is still an important pillar in the education of a developer and there might be times when mastering this topic may come handy.
Synchronous Task
A synchronous task may well be pictured in the following picture.
There is no parallelism at all and everything starts and ends with the same thread handling the work.
To run a task synchronously means blocking waiting for the task to end.
The usage of asynchronous code is a means to efficiency.
Asynchronous Tasks
A definition of what an asynchronous task could be:
Asynchronous programming is a means of parallel programming in which a unit of work runs separately from the main application thread and notifies the calling thread of its completion, failure or progress.
Running a task asynchronously means not blocking waiting for its end.
IO Bound Operations
IO Bound operations are tasks that generally carry a high latency with them. Most of the time taken by the task would be spent waiting.
A typical example are HTTP Calls.
In .NET, they are shipped to the network interface from the OS. On Windows, that works with IRP, Interrupt Request Packets.
After sending the data to the device, the OS Thread becomes available to be reused. When the networking operation will end, the data will be returned and the .NET Runtime notified.
Throughout this entire process, a key takeaway is that no thread is dedicated to running the task.
The final result is not having threads idle waiting.
CPU Bound Operations
When a task involves computational work, the associated thread is allocated to a CPU Core and it cannot be used for other work.
In this scenario, there is no room for savings. There is no big latency involved and the challenge becomes breaking down the task into smaller units that can be run on different cores.
The typical way of creating computational tasks in C# is by using the Task class.
Server Side operations
Asynchronous programming in a server scenario allows to save threads that may be used to handle other requests, preventing the server from becoming unresponsive under heavy loads.
Consider two servers: one that runs async code, and one that does not. For the purpose of this example, each server only has 5 threads available to service requests. Note that these numbers are imaginarily small and serve only in a demonstrative context.
Assume both servers receive 6 concurrent requests. Each request performs an I/O operation. The server without async code has to queue up the 6th request until one of the 5 threads have finished the I/O-bound work and written a response. At the point that the 20th request comes in, the server might start to slow down, because the queue is getting too long.
(..)
The server with async code running on it still queues up the 6th request, but because it uses
async
andawait
, each of its threads are freed up when the I/O-bound work starts, rather than when it finishes. By the time the 20th request comes in, the queue for incoming requests will be far smaller (if it has anything in it at all), and the server won't slow down.
Asynchronous programming server-side does not make as much sense for CPU bound operations.
A good explanation of why this might not be the best idea may be found here:
However, it's bad to use it for CPU-bound methods. Sometimes devs think they can get the benefits of
async
by just callingTask.Run
in their controllers, and this is a horrible idea. Because that code ends up freeing up the request thread by taking up another thread, so there's no benefit at all (and in fact, they're taking the penalty of extra thread switches)!
Client Side operations
Asynchronous code running on clients is typically needed to avoid for the UI to freeze. That would be true both for IO bound operations and CPU bound operations:
CPU-bound
async
code is a bit different than I/O-boundasync
code. Because the work is done on the CPU, there's no way to get around dedicating a thread to the computation. The use ofasync
andawait
provides you with a clean way to interact with a background thread and keep the caller of the async method responsive.
The alternative programming model would be creating new threads to handle operations. While this would work, it would not be as efficient and it would cause new threads to be created instead of those existing to be reused.
More importantly, because I/O-bound work spends virtually no time on the CPU, dedicating an entire CPU thread to perform barely any useful work would be a poor use of resources.
JavaScript
JavaScript sits on a different computational model, the so called “Event Loop”.
The event loop looks pretty much as follows:
while (queue.waitForMessage()) {
queue.processNextMessage()
}
Each message has an associated function which gets called in order to handle the message.
(…)
In web browsers, messages are added anytime an event occurs and there is an event listener attached to it. If there is no listener, the event is lost.
So a click on an element with a click event handler will add a message — likewise with any other event.
The JavaScript engine is inherently asynchronous and everything is run by triggering events and processing messages that come with functions attached.
CPU and IO bound operations
CPU and IO bound operations are executed differently that in .NET.
CPU bound operations would typically be run using web workers.
IO bound operations, like AJAX calls, are performed using asynchronous XHRs.
JavaScript, like .NET with Tasks, implements the “Promise Model of Concurrency”:
Tasks are constructs used to implement what is known as the Promise Model of Concurrency. In short, they offer you a “promise” that work will be completed at a later point, letting you coordinate with the promise with a clean API.
The language construct used are called promises:
Summary
Asynchronous programming is a very challenging and interesting topic.
Many frameworks came with their solutions to make the creation of asynchronous code simple for the developer and is very easy creating code that runs asynchronously.