Asynchronous Programming in C#

Tasks

In .NET, the Task class enables us to run jobs asynchronously. It offers a more high-level framework than we saw in the previous post on threading, which means we don’t have to get involved with mutexes and signalling.

There are two versions of the task class: the non-generic Task class which is for tasks which don’t have a return type, and the generic Task<T> class which is for tasks which return a type T.

The Task class has several constructors, including:

public Task(Action action);
public Task(Action<Object> action);

The second version of the constructor allows data to be passed into the task.

Similarly, the generic Task<T> includes the following constructors:

public Task<T>(Func<T> func);
public Task<T>(Func<Object, T> func);

where the template parameter T specifies the return type of the task.

In the following example, we create a task using a lambda expression and run it:

Task task = new Task(() => { Console.WriteLine("Hello"); });
task.Start();

Note a call Start() will run the task asynchronously so the calling thread will just continue without waiting for the task to finish.

To wait for a task to complete, call task.Wait().

Suppose we want to create a task which takes a string input parameter and returns a bool. A Task<bool> object could be defined using a lambda expression as follows:

Task<bool> task = new Task<bool>(x => (string)x == "Alice", "Bob");

Here, the underlying Action is to take the input object, cast it to a string, and compare it to the string "Alice", returning true or false.

For tasks which have a return type, we can call Result() to get the result. If the task has not yet completed, the calling thread will be suspended until the result is available.

async and await

C# 4.5 introduced two new keywords, async and await.

To use the await keyword, you need to first mark your method as async. For example:

public async void MakeHttpRequest()
{
}

Note that code written prior to C# 4.5 may well use await as a variable name. So the async keyword was introduced to ensure that existing code (which may legally use await as a variable name) will still compile.

The easiest way to explain async and await is to look at an example:

class Example
{
    public void Action1()
    {
        Console.WriteLine("Performing Action1...");
        Thread.Sleep(1000);
        Console.WriteLine("Finished Action1");
    }

    public void Action2()
    {
        Console.WriteLine("Performing Action2...");
        Thread.Sleep(1000);
        Console.WriteLine("Finished Action2");
    }

    public async void PerformTasks()
    {
        await Task.Run(Action1);
        await Task.Run(Action2);
    }

    public void RunExample()
    {
        Task task = PerformTasks();
        Console.WriteLine("Press a key...");
        Console.ReadKey();
    }
}

When the await keyword is encountered, the first task is started and will run on a background thread. However, while PerformTasks() is waiting for the task to complete, the calling method RunExample() is able to get on with what it was doing. So in this example, "Press a key" will appear on the console.

Note that both PerformTasks() and RunExample() run on the same thread. When the first task is complete, PerformTasks is able to continue where it left off and move onto the second task.

In the example, we had to add a call to Console.ReadKey() to pause execution, otherwise the program will exit before Action1() and Action2() are completed.

Instead of relying on the user to press a key, we could simply change PerformTasks() to return a Task instead of a void and wait for the task to complete. The changed functions would be as follows:

public async Task PerformTasks()
{
    await Task.Run(Action1);
    await Task.Run(Action2);
}

public void RunExample()
{
    Task task = PerformTasks();
    task.Wait();
}