October 23, 2019

Parallel programming in C#

In this post I want to explore three ways of making asynchronous calls in C# with a for-loop. Async calls can be very effective to up performance on certain tasks, especially when those tasks involve making calls over the network. But it’s important to keep in mind that asynchronous calls increases the complexity, and demands more resources. So before utilizing it, question whether or not it’s the right tool for your specific case.

Before we get started, I want to define the Worker class that is going to be used to simulate performing a time-consuming task.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  public class Worker {
    private int num;
    public Worker(int num) {
      this.num = num;
    }

    public Task<int> DoWork() {
      return Task.Factory.StartNew(() => {
        Console.WriteLine("Started work");
        Thread.Sleep(500);
        return this.num;
      });
    }
  }

Then we can prepare a simple list of 500 workers, ready to do some work.

1
2
3
4
5
6
7
8
9
10
  public class Context {
    public async void Execute() {
      var workers = new List<Worker>();
      var nbrOfWorkers = 500;

      while (nbrOfWorkers-- > 0) {
        workers.Add(new Worker(nbrOfWorkers));
      }
    }
  }

WhenAny

The first method is to use a list of Tasks and only continue execution as soon as any task in the list has completed its work. This requires one foreach loop to start the tasks and one foreach loop to handle the results. Tasks will be started asynchronously together with each other, but as soon as one task has completed its execution the result of it will be handled.

1
2
3
4
5
6
7
8
9
10
11
var tasks = new List<Task<int>>(); // Setup the task list

foreach (var worker in workers) {
  tasks.Add(worker.DoWork()); // Start all the tasks asynchronously
}

await Task.WhenAny(tasks); // Wait for any of the tasks in the task list to be complete

foreach (var task in tasks) {
  Console.WriteLine(task.Result); // Handle the result from the task
}

.ContinueWith

Another option is to chain the task with .ContinueWith.

1
2
3
4
5
foreach (var worker in workers) {
  worker.DoWork().ContinueWith((prev) =&gt; {
    Console.WriteLine(prev.Result);
  });
}

This approach doesn’t require you to store a list of tasks, unless you want to stall execution of coming code until all tasks are completed. The result is much less code, but it can also be slightly harder to read and maintain.

The code is finally executed in the main method like this.

1
2
3
4
5
6
7
public class Program {
    static void Main(string[] args) {
      var context = new Context();
      context.Execute();
      Console.ReadKey();
    }
  }

When timed with a StopWatch, the results were that using .ContinueWith was a bit faster which took 26704 ms, compared to the WhenAny approach which completed in 33269 ms.

An advantage with .WhenAny seemed to be that it executed each task in the same order they were started, whereas .ContinueWith executed a bit more randomly.

Async streams

With C# 8 there is something called async streams which is a great feature. With this update, the code becomes much simpler to read. Just add the ‘await’ keyword in front of the foreach loop.

1
2
3
await foreach (var worker in workers) {
  Console.WriteLine(worker);
}

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *