This content originally appeared on SitePoint and was authored by Kerem Ispirli
The async and await keywords were introduced in C# to make asynchronous programming on the .NET platform easier. These keywords have fundamentally changed how code is written in most of the C# ecosystem. Asynchronous programming has become mainstream, and modern frameworks such as ASP.NET Core are fully asynchronous.
Having such an impact on the C# ecosystem, asynchronous programming proves to be quite valuable. But what is asynchronous programming in the first place?
This article is going to introduce asynchronous programming, show usage of async and await keywords, talk about the deadlock pitfall and finish with some tips for refactoring blocking C# code with these keywords.
Let’s start with terminology.
Concurrent vs Parallel vs Asynchronous
What are the differences between these three terms? All of them are applications of multi-threading, their definitions overlap, and they are often used interchangeably. That’s why the terminology for implementations that leverage multi-threading can be confusing.
We’ll go through the nuances between these terms, so that we can come up with a clear definition for asynchronous programming.
Let’s assume a GUI application as an example.
Synchronous execution: doing things one after the other
The user clicks a button and waits for the application to finish handling the click event. Since only one thing can happen at a time, the UI stops responding until the event has been completely handled. In the same way, the application can’t do anything in the background while UI is available for user input.
Concurrent: doing multiple things at the same time
The user clicks a button, and the application triggers a separate thread in the background to execute the task needed to satisfy user’s request concurrently. The thread responsible for handling UI events becomes available again immediately after starting the background thread, keeping the UI responsive.
Parallel: doing multiple copies of something at the same time
The user instructs the application to process all the files in a folder. The application triggers a number of threads with the processing logic and distributes the files among these threads.
Asynchronous: not having to wait for one task to finish before starting another
The application starts a database query asynchronously. While the query is in progress, it also starts reading a file asynchronously. While both tasks are in progress, it does some calculation.
When all these tasks are finished, it uses the results of all these three operations to update the UI.
Asynchronous Programming
Based on the terminology above, we can define asynchronous programming simply as follows:
The execution thread should not wait for an I/O-bound or CPU-bound task to finish.
Examples of I/O-bound operations can be file system access, DB access, or an HTTP request. Examples of CPU-bound operations can be resizing an image, converting a document, or encrypting/decrypting data.
Benefits
Using asynchronous programming has several benefits:
- avoiding thread pool starvation by “pausing” execution and releasing the thread back to thread pool during asynchronous activities
- keeping the UI responsive
- possible performance gains from concurrency
Asynchronous Programming Patterns
.NET provides three patterns for performing asynchronous operations.
Asynchronous programming model (APM): LEGACY
Also known as an IAsyncResult pattern, it’s implemented by using two methods: BeginOperationName and EndOperationName.
public class MyClass {
public IAsyncResult BeginRead(byte [] buffer, int offset, int count, AsyncCallback callback, object state) {...};
public int EndRead(IAsyncResult asyncResult);
}
From the Microsoft documentation:
After calling
BeginOperationName, an application can continue executing instructions on the calling thread while the asynchronous operation takes place on a different thread. For each call toBeginOperationName, the application should also callEndOperationNameto get the results of the operation.
Event-based asynchronous pattern (EAP): LEGACY
This pattern is implemented by writing an OperationNameAsync method and an OperationNameCompleted event:
public class MyClass {
public void ReadAsync(byte [] buffer, int offset, int count) {...};
public event ReadCompletedEventHandler ReadCompleted;
}
The asynchronous operation will be started with the async method, which will trigger the Completed event for making the result available when the async operation is completed. A class that uses EAP may also contain an OperationNameAsyncCancel method to cancel an ongoing asynchronous operation.
Task-based asynchronous pattern (TAP): RECOMMENDED
We have only an OperationNameAsync method that returns a Task or a generic Task<T> object:
public class MyClass {
public Task<int> ReadAsync(byte [] buffer, int offset, int count) {...};
}
Task and Task<T> classes model asynchronous operations in TAP. It’s important to understand Task and Task<T> classes for understanding TAP, which is important for understanding and using async/await keywords, so let’s talk about these two classes in more detail.
Task and Task<T>
The Task and Task<T> classes are the core of asynchronous programming in .NET. They facilitate all kinds of interactions with the asynchronous operation they represent, such as:
- adding continuation tasks
- blocking the current thread to wait until the task is completed
- signaling cancellation (via
CancellationTokens)
After starting an asynchronous operation and getting a Task or Task<T> object, you can keep using the current execution thread to asynchronously execute other instructions that don’t need the result of the task, or interact with the task as needed.
Here’s some example code that uses tasks to visualize what it looks like in action:
using System;
using System.Threading.Tasks;
public class Example {
public static void Main() {
Task<DataType> getDataTask = Task.Factory.StartNew(() => { return GetData(); } );
Task<ProcessedDataType> processDataTask = getDataTask.ContinueWith((data) => { return ProcessData(data);} );
Task saveDataTask = processDataTask.ContinueWith((pData) => { SaveData(pData)} );
Task<string> displayDataTask = processDataTask.ContinueWith((pData) => { return CreateDisplayString(pData); } );
Console.WriteLine(displayDataTask.Result);
saveDataTask.Wait();
}
}
Let’s walk through the code:
- We want to get some data. We use
Task.Factory.StartNew()to create a task that immediately starts running. This task runsGetData()method asynchronously and, when finished, it assigns the data to its.Resultproperty. We assign this task object togetDataTaskvariable. - We want to process the data that
GetData()method will provide. Calling.ContinueWith()method, we asynchronously create another task and set it as a continuation togetDataTask. This second task will take the.Resultof the first task as an input parameter (data) and call theProcessData()method with it asynchronously. When finished, it will assign the processed data to its.Resultproperty. We assign this task to theprocessDataTaskvariable. (It’s important to note that, at the moment, we don’t know whethergetDataTaskis finished or not, and we don’t care. We just know what we want to happen when it’s finished, and we write the code for that.) - We want to save the processed data. We use the same approach to create a third task that will call
SaveData()asynchronously when data processing is finished, and set it as a continuation toprocessDataTask. - We also want to display the processed data. We don’t have to wait for the data to be saved before displaying it, so we create a fourth task that will create the display string from the processed data asynchronously when data processing is finished, and set it also as a continuation to
processDataTask. (Now we have two tasks that are assigned as continuations toprocessDataTask. These tasks will start concurrently as soon asprocessDataTaskis completed.) - We want to print the display string to the console. We call
Console.WriteLine()with.Resultproperty of thedisplayDataTask. The.Resultproperty access is a blocking operation; our execution thread will block untildisplayDataTaskis completed. - We want to make sure that the data is saved before leaving the
Main()method and exiting the program. At this point, though, we do not know the state ofsaveDataTask. We call the.Wait()method to block our execution thread untilsaveDataTaskcompletes.
Almost Good
As demonstrated above, TAP and Task/Task<T> classes are pretty powerful for applying asynchronous programming techniques. But there’s still room for improvement:
- Boilerplate code needed for using tasks is quite verbose.
- Assigning continuations and making granular decisions about which task should run means a lot of details should be handled by the programmer, increasing the complexity and making the code error-prone. (Verbosity, combined with increased complexity, means the code will be difficult to understand, thus difficult to maintain.)
- Despite all this power, there’s no way to wait for a task to complete without blocking the execution thread.
These drawbacks can become significant challenges for teams to adopt TAP.
This is where the async and await keywords come into play.
Continue reading Asynchronous Programming Using Async/Await in C# on SitePoint.
This content originally appeared on SitePoint and was authored by Kerem Ispirli
Kerem Ispirli | Sciencx (2021-02-03T15:00:31+00:00) Asynchronous Programming Using Async/Await in C#. Retrieved from https://www.scien.cx/2021/02/03/asynchronous-programming-using-async-await-in-c-2/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.