Along with these new keywords, Microsoft also added a plethora of methods across the .NET Framework that end with the suffix Async. As the suffix hints, these methods are all asynchronous and they are used in conjunction with the new keywords. Let's start with the basic rules.
First of all, in order to use the await keyword in a method, the method signature must be declared with the async keyword. The async keyword enables us to use the await keyword in the method without error and is responsible for returning just the T generic type parameter from asynchronous methods whose signatures declare a return type of Task<T>. A method that is modified with the async keyword is known as an async method.
Async methods actually execute in a synchronous manner, until they reach an await expression. If there is no await keyword in the method, then the whole method will run synchronously and the compiler will output a warning.
While a portion of async methods run asynchronously, they don't in fact run on their own threads. No additional threads are created using the async and await keywords. Instead, they give the appearance of multithreading by using the current synchronization context, but only when the method is active and not when it is paused, while running an await expression.
When execution reaches an await keyword, the method is suspended until the awaited task has completed asynchronously. During this time, execution returns to the method caller. When the asynchronous action is complete, program execution returns to the method and the remainder of the code in it is run synchronously.
Async methods are required to have a particular signature. They all need to use the async modifier keyword, and in addition to this the names of async methods should end with the Async suffix to clearly signify that they are asynchronous methods. Another requirement of declaring async methods is that they cannot contain any ref or out input parameters.
The final requirement is that async methods can only use one of three return types: Task, the generic Task<TResult>, or void. Note that the generic type TResult parameter is the same as and can be replaced with T, but Microsoft refers to it as TResult simply because it specifies a return type.
All async methods that return some meaningful result will use type Task<TResult>, where the actual type of the return value will be specified by the TResult generic type parameter. Therefore, if we want to return a string from our async method, we declare that our async method returns a parameter of type Task<string>. Let's see an example of this in action:
using System; using System.IO; using System.Threading.Tasks; ... public async Task<string> GetTextFileContentsAsync(string filePath) { string fileContents = string.Empty; try { using (StreamReader streamReader = File.OpenText(filePath)) { fileContents = await streamReader.ReadToEndAsync(); } } catch { /*Log error*/ } return fileContents; }
Here we have a simple async method that returns a string that represents the contents of the text file specified by the filePath input parameter. Note that the actual return type of the method is in fact Task<string>. In it, we first initialize the fileContents variable and then attempt to create a StreamReader instance from the File.OpenText method within the using statement.
Inside the using statement, we attempt to populate the fileContents variable by awaiting the result of the ReadToEndAsync method of the StreamReader class. Up until this point, the method will run synchronously. The ReadToEndAsync method will be called, and then control will immediately return to the caller of our async method.
When the return value of the ReadToEndAsync method is ready, execution returns to our async method and continues where it left off. In our example, there is nothing else to do but return the result string, although async methods can contain any number of lines after the await keyword, or even multiple await keywords. Note that in a real-world application, we would log any exceptions that might be thrown from this method.
If our async method just performs some function asynchronously, but does not return anything, then we use a return type of Task. That is, the task-based async method will return a Task object that enables it to be used with the await keyword, but the actual method will not return anything to the caller of that method. Let's see an example of this:
using System.Text; ... public async Task SetTextFileContentsAsync(string filePath, string contents) { try { byte[] encodedFileContents = Encoding.Unicode.GetBytes(contents); using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096, true)) { await fileStream.WriteAsync(encodedFileContents, 0, encodedFileContents.Length); } } catch { /*Log error*/ } }
In the SetTextFileContentsAsync method, we first need to convert our input string to a byte array. For this reason, we now need to add a using directive for the System.Text namespace in addition to the three originally specified. Note that in this particular example, we are using Unicode encoding, but you are free to use any other encoding value here.
After using the GetBytes method to obtain a byte array from the contents input parameter, we initialize a new FileStream object within another using statement. Apart from the bool useAsync input parameter, the remaining parameters used in the FileStream constructor in this example are unimportant, and you are free to replace them with values that suit your requirements better.
Inside the using statement, we see the await keyword used with the WriteAsync method. Up until this point, this method will run synchronously, and on this line it will start execution of the WriteAsync method and then return control to the method caller.
As execution leaves the using statement, the FileStream instance will be closed and disposed of. As this method has nothing to return, the return type of the async method is Task, which enables it to be awaited by the calling code. Again, we would typically log any exceptions that might be thrown from this method, but this is omitted here for brevity.
Most of us will never use the third return type option of void when using MVVM, because it is primarily used in event handling methods. Note that async methods that return void cannot be awaited and that calling code cannot catch exceptions thrown from such async methods.
One of the most commonly asked questions regarding async methods is "How can I create an async method from a synchronous method?" Luckily, there is a very simple solution to this using the Task.Run method, so let's take a quick look at it now:
await Task.Run(() => SynchronousMethod(parameter1, parameter2, etc));
Here we use a Lambda expression to specify the synchronous method to run in an asynchronous context. That's all that we have to do to run a synchronous method asynchronously. However, what about the opposite requirement? Let's now see how we can run an asynchronous method synchronously. Again, the Task class provides us with a solution:
Task task = SetFileContentsAsync(filePath, contents); task.RunSynchronously();
As we saw at the end of Chapter 1, A Smarter Way of Working with WPF, in order to run an asynchronous method synchronously, we first need to instantiate a Task instance from our asynchronous method. Then, all we have to do is call the RunSynchronously method on that instance, and it will run synchronously.