ZetCode

C# Task

last modified June 17, 2022

In this article, we show how to use Task for concurrent operations in C#.

Concurrent programming is used for two kinds of tasks: I/O-bound and CPU-boud tasks. Requesting data from a network, accessing a database, or reading and writing are IO-bound tasks. CPU-boud tasks are tasks that are computationally expensive, such as mathematical calculations or graphics processing.

Asynchronous operations are suited for I/O-bound tasks. Parallel operations are suited for CPU-bound tasks. Unlike in other languages, Task can be used for both asynchronous and parallel operations.

Note: the MSDN documentation incorrecly uses the term asynchronous instead of the standardly used concurrent.

Task represents a concurrent operation.

Task
Task<TResult>

Task represents an concurrent operation, while Task<TResult> represents an concurrent operation that can return a value.

The Task.Run method is used to run CPU-bound code concurrently; ideally in parallel. It queues the specified work to run on the ThreadPool and returns a task or Task<TResult> handle for that work.

.NET contains numerous methods such as StreamReader.ReadLineAsync or HttpClient.GetAsync that execute I/O-bound code asynchronously. They are used together with async/await keywords.

Note: it is recommended to use use Task.Run instead of using new Task(); task.Start().

C# Task.Run

The Task.Run method puts a task on a different thread. It is suitable for CPU-bound tasks.

Program.cs
Console.WriteLine($"Main thread {getThreadId()} begin");

Task.Run(() =>
{
    Console.WriteLine($"Thread {getThreadId()} begin");

    Thread.Sleep(3000);

    Console.WriteLine($"Thread {getThreadId()} end");
});

Console.WriteLine($"Main thread {getThreadId()} end");

Console.ReadLine();

int getThreadId() 
{
    return Thread.CurrentThread.ManagedThreadId;
}

The main thread finishes before the generated task. In order to see the task finished, we use Console.ReadLine which waits for user input.

$ dotnet run
Main thread 1 begin
Main thread 1 end
Thread 4 begin
Thread 4 end

Task<TResult> represents a task which returns a result.

Program.cs
Task<int> task = Task.Run(() =>
{
    Thread.Sleep(3000);
    return 2 + 3;
});

var res = await task;
Console.WriteLine(res);

The program shows how to wait for a task that returns a computation result.

C# Task.Delay

Task.Delay creates a task which completes after a time delay.

Program.cs
Console.WriteLine("step 1");

await doTask();

Console.WriteLine("step 2");

async Task doTask()
{
    await Task.Delay(3000);
    Console.WriteLine("task finished");
}

The function which creates a task must use the async keyword.

await Task.Delay(3000);

Task.Delay creates a new task, which sleeps for three seconds. The await operator waits for the task to finish. It block execution of the main program until the task is finished.

$ dotnet run
step 1
task finished
step 2

C# async Main method

When we are using the await operator inside the Main method, we have to mark it with the async modifier.

words.txt
sky
main
club
cotton
rocket

This is a sample text file.

Program.cs
namespace AsyncMain;

class Program
{
    static async Task Main(string[] args)
    {
        using StreamReader reader = File.OpenText("words.txt");
        string? res = await reader.ReadLineAsync();

        Console.WriteLine($"First line is: {res}");
    }
}

The example reads the first line of a file asynchronously. The work is done inside the Main method.

string? res = await reader.ReadLineAsync();

The ReadLineAsync method returns a Task<String> that represents an asynchronous read operation. The result in a task contains the next line from the stream, or is null if all the characters have been read.

$ dotnet run
First line is: sky

C# Task.WaitAll

The Task.WaitAll method waits for all of the provided tasks to complete execution.

Program.cs
using System.Diagnostics;

var sw = new Stopwatch();
sw.Start();

Task.WaitAll(f1(), f2(), f3());

sw.Stop();

var elapsed = sw.ElapsedMilliseconds;
Console.WriteLine($"elapsed: {elapsed} ms");

async Task f1()
{
    await Task.Delay(4000);
    Console.WriteLine("f1 finished");
}

async Task f2()
{
    await Task.Delay(7000);
    Console.WriteLine("f2 finished");
}

async Task f3()
{
    await Task.Delay(2000);
    Console.WriteLine("f3 finished");
}

We measure the execution time of three asynchronous methods.

Task.WaitAll(f1(), f2(), f3());

The Task.WaitAll waits for all of the provided tasks to complete execution.

$ dotnet run
f3 finished
f1 finished
f2 finished
elapsed: 7000 ms

C# Task.ContinueWith

The Task.ContinueWith creates a continuation that executes asynchronously when the target Task<TResult> completes.

Program.cs
Task<int> task = Task.Run(() => 
    runTask()).ContinueWith<int>((x) => x.Result * 2);
var res = await task;

Console.WriteLine(res);

int runTask()
{
    int x = 1;
    int y = 2;
    int z = 3;

    Thread.Sleep(1000);
    return x + y + z;
}

In the example, we chain two operations with ContinueWith.

C# mulitple async requests

The HttpClient class is used for sending HTTP requests and receiving HTTP responses from the specified resource.

Program.cs
var urls = new string[] { "http://webcode.me", "http://example.com",
    "http://httpbin.org", "https://ifconfig.me", "http://termbin.com",
    "https://github.com"
};

using var client = new HttpClient();

var tasks = new List<Task<HttpResponseMessage>>();

foreach (var url in urls)
{
    tasks.Add(client.GetAsync(url));
}

Task.WaitAll(tasks.ToArray());

var data = new List<HttpResponseMessage>();

foreach (var task in tasks)
{
    data.Add(await task);
}

foreach (var res in data)
{
    Console.WriteLine(res.StatusCode);
}

We send asynchronous GET requests to various web pages and get their response status codes.

tasks.Add(client.GetAsync(url));

The GetAsync sends a GET request to the specified url and returns the response body in an asynchronous operation. It returns a new task. The task is added to the list of tasks.

Task.WaitAll(tasks.ToArray());

The Task.WaitAll waits for all of the provided tasks to complete execution.

data.Add(await task);

The await unwraps the result of the operation.

foreach (var res in data)
{
    Console.WriteLine(res.StatusCode);
}

We print the status of each request.

$ dotnet run
OK
OK
OK
OK
OK
OK

In this tutorial, we have used Task for concurrent operations in C#.

Read C# tutorial or list all C# tutorials.