Execute tasks in parallel using async and await to speed up computation

Suppose you have five tasks you want your computer to execute and that each takes 1 second to execute.

In the old days, one would figure that each task would be executed sequentially, resulting in a total execution time of 5 seconds.

Even today, many programmers would write a program to execute these tasks sequentially. It’s easier. In the back of their mind they might have heard of things like multi threading and threadpools and such, but that’s the realm of advanced programming. Indeed, over the years there have been several programming models to make this easier. Here are some:

1. CreateThread (C++)

2. System.ThreadingThread

3. BackgroundWorker

4. ThreadPool.QueueUserWorkItem

5. Windows WorkFlow Foundation Activities

Let’s make it easier.

Below is some code that shows a window with some TextBoxes, in which you can specify the number of tasks and their duration. The Go button will execute the tasks. If you execute them in parallel, all five execute in 1 second!

Each task is pretty simple:

1. Show a status message indicating the task is starting

2. Do the actual work: either of:

a. Sleep the current thread for the task duration

b. Execute a nested loop multiple times

3. Show a message indicating the task is ending, including the elapsed time.

There’s a CheckBox that will execute the tasks Sequentially, or in parallel.

A calculation is made to determine how many loops to execute to get approximately one second of work. Thus we can make a task be CPU intensive for the specified number of seconds.

Caveats:

1. The tasks in this sample are idle, causing very little actual consumption of resources, like CPU cycles, memory or disk accesses.

2. The tasks can be run in parallel with very little contention for shared resources (except for the shared status messaging)

a. Real world tasks typically have more contention for a shared resource, such as a shared data structure, I/O (like a file) or User Interface

Apart from these caveats, the sample illustrates a framework for writing parallel executing code

1. The AddStatusMsg routine gets called from various threads, but updates the UI on the Main thread, showing the thread ID of the caller.

2. C# Async and Await are used to create the tasks Asynchronously, keeping the UI thread responsive, even when running the tasks.

3. An async Lambda is used for the Button Go Click event.

4. The System.Threading.Tasks.Task class is used to store and manipulate the tasks.

Start Visual Studio

File->New Project->C# Windows WPF application

Replace all the code in MainWindow.xaml.cs with the code below.

Try running the tasks using various parameters. With 5 1 second tasks executed sequentially, it takes just a little more than 5 seconds as expected. Running them in parallel yields 1.01 seconds of time.

I can run 60 tasks of 1 second each, and it takes only 2 seconds total.

There are many more facets of this to explore:

1. Make the work non-trivial.

2. Add some more resource contention

3. Add Cancellation capability

4. Explore Adding continuation tasks with ContinueWith

<code>

 using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication1
{
  public partial class MainWindow : Window
  {
    public TextBox _txtStatus;
    int _nOneSecondOfWork = 0;
    public MainWindow()
    {
      InitializeComponent();
      this.Loaded += (ol, el) =>
      {
        try
        {
          this.Top = 0;
          this.Height = 900;
          this.Width = 600;
          var grid = new Grid();
          grid.RowDefinitions.Add(
              new RowDefinition()
                  {
                    Height = ((GridLength)
                    (new GridLengthConverter())
                    .ConvertFromString("30"))
                  }
              );
          grid.RowDefinitions.Add(
              new RowDefinition()
                  {
                    Height = ((GridLength)
                    (new GridLengthConverter())
                    .ConvertFromString("*"))
                  }
              );
          var spControls = new StackPanel()
          {
            Orientation = Orientation.Horizontal,
            Height = 25,
            VerticalAlignment = VerticalAlignment.Top
          };
          Grid.SetRow(spControls, 0);
          grid.Children.Add(spControls);
          this.Content = grid;
          spControls.Children.Add(
              new Label()
              {
                Content = "# Tasks"
              });
          var txtNumTasks = new TextBox()
              {
                Text = "5",
                Width = 100
              };
          spControls.Children.Add(txtNumTasks);
          spControls.Children.Add(
              new Label()
              {
                Content = "Duration of task"
              });
          var txtTaskDuration = new TextBox()
          {
            Text = "1",
            Width = 100,
            ToolTip = "Duration of task in seconds"
          };
          spControls.Children.Add(txtTaskDuration);
          var chkSequential = new CheckBox()
          {
            Content = "_Sequential",
            IsChecked = true
          };
          spControls.Children.Add(chkSequential);
          var chkDoRealWork = new CheckBox()
          {
            Content = "_DoRealWork",
            IsChecked = false
          };
          spControls.Children.Add(chkDoRealWork);
          var btnGo = new Button()
          {
            Content = "_Go"
          };
          spControls.Children.Add(btnGo);
          _txtStatus = new TextBox()
          {
            IsReadOnly = true,
            VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
            HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
            IsUndoEnabled = false,
            MaxHeight = 800,
            HorizontalContentAlignment = HorizontalAlignment.Left
          };
          grid.Children.Add(_txtStatus);
          Grid.SetRow(_txtStatus, 1);
          AddStatusMsg("Click Go Button to start tasks with work");

          btnGo.Click += async (ob, eb) =>
              {
                try
                {
                  int numTasks = int.Parse(txtNumTasks.Text);
                  int taskDuration = int.Parse(txtTaskDuration.Text);
                  bool fSequential = chkSequential.IsChecked.Value;
                  bool fDoRealWork = chkDoRealWork.IsChecked.Value;
                  AddStatusMsg(
                      "Starting Work #Tasks={0} Dura={1}  Seq={2} DoWork = {3}",
                      numTasks,
                      taskDuration,
                      fSequential,
                      fDoRealWork
                      );
                  await DoWork(
                      numTasks,
                      taskDuration,
                      fSequential,
                      fDoRealWork
                      );
                }
                catch (Exception ex)
                {
                  AddStatusMsg(ex.ToString());
                }
              };
        }
        catch (Exception ex)
        {
          this.Content = ex.ToString();
        }
      };
    }

    async Task<int> DoWork(
        int nTasks,
        int nTaskDuration,
        bool fsequential,
        bool fDoRealWork)
    {
      DateTime dtStart = DateTime.Now;
      Action work = () =>
      {
        if (fDoRealWork)
        {
          CalculateOneSecondOfWork();
        }
        dtStart = DateTime.Now;
        var tsklist = new List<Task>();
        for (int nTask = 0; nTask < nTasks; nTask++)
        {
          var tsknum = nTask;
          var tsk = Task.Run(() =>
          {
            AddStatusMsg("start task {0}", tsknum);
            var dtTaskStart = DateTime.Now;
            if (fDoRealWork)
            {
              DoSomeWork(_nOneSecondOfWork * nTaskDuration);
            }
            else
            {
              Thread.Sleep(
                  TimeSpan.FromSeconds(nTaskDuration));
            }
            var elapsedTask = DateTime.Now - dtTaskStart;
            AddStatusMsg("end   task {0} {1:n4}",
                tsknum,
                elapsedTask.TotalSeconds);
          });
          tsklist.Add(tsk);
          if (fsequential)
          {
            tsk.Wait();
          }
        }
        Task.WaitAll(tsklist.ToArray());
      };
      await Task.Run(work);
      var elapsed = DateTime.Now - dtStart;
      AddStatusMsg("All Work done in {0}", elapsed.TotalSeconds);
      return 0;
    }

    void CalculateOneSecondOfWork()
    {
      AddStatusMsg("Calculating loop count");
      var start = DateTime.Now;
      // calculate how many loops will take one second
      // by timing 1000 
      DoSomeWork(1000);
      var elapsed = DateTime.Now - start;
      _nOneSecondOfWork = (int)(1000 / elapsed.TotalSeconds);
      AddStatusMsg("Loop count for 1 second of work = {0}",
          _nOneSecondOfWork);
    }
    void DoSomeWork(int n)
    {
      for (int i = 0; i < n; i++)
      {
        for (int j = 0; j < 1000000; j++)
        {
        }
      }
    }
    void AddStatusMsg(string msg, params object[] args)
    {
      if (_txtStatus != null)
      {
        // we want to read the threadid 
        //and time immediately on current thread
        var dt = string.Format("[{0}],{1,2},",
            DateTime.Now.ToString("hh:mm:ss:fff"),
            Thread.CurrentThread.ManagedThreadId);
        _txtStatus.Dispatcher.BeginInvoke(
            new Action(() =>
        {
          // this action executes on main thread
          var str = string.Format(dt + msg + "\r\n", args);
          _txtStatus.AppendText(str);
          _txtStatus.ScrollToEnd();
        }));
      }
    }
  }
}

</code>