Writing thread-safe event handlers with the Task Parallel Library in .NET 4.0

Microsoft .NET

Download Example Code
TasksWithThreadSafeEvents.zip

In this article we will be using the following technologies:

  • .NET 4.0
  • Windows Forms (WinForms)
  • Task Parallel Library (TPL, part of .NET 4.0)

In a nutshell, I am talking about writing thread-safe events for WinForms. I've not ventured into the world of WPF quite yet, so this article may or may not apply to WPF.

Now, with that said, you may be familiar with the concept of BeginInvoke, EndInvoke, and Invoke to access WinForms controls safely from other threads. The amount of code to do that can be quite cumbersome, and to me it looks like speghetti. Another way to do it was by using ISynchronizeInvoke which you could wrap into a helper class, and do invoking in a single line of code. These methods all work great.

There are quite a few articles that explain how to do thread-safe and synchronized events with the Task Parallel Library, but they often are long and complicated. The other problem with the majority of these articles out there all assume one, single, horrific thing: You will always be writing your parallel code inside the form and have access to your controls other other referencable objects. Furthermore, they all seem to implement some sort of helper class that comes as an extra. In this article, my aim is a bit more specific. Take the System.Net.WebClient class for example. It exposes an event called DownloadProgressChanged. You know what is great about this? It's thread-safe, and it doesn't have a clue about your form or controls. That's what this article is about. I am going to show you how to write a class with completely thread-safe events using the TPL and just a few lines of code.

The code:

//-----------------------------------------------------------------------------
// <copyright file="Counter.cs" company="DCOM Productions">
//     Copyright (c) DCOM Productions.  All rights reserved.
// </copyright>
//-----------------------------------------------------------------------------

namespace TasksWithThreadSafeEvents.Objects {
    using System;
    using System.Threading.Tasks;
    using System.Threading;

    internal class Counter {
        // CLR generated constructor

        #region Events

        /// <summary>
        /// Occurs when the counter has counted
        /// </summary>
        public event EventHandler CountChanged;
        private void OnCountChanged() {
            if (CountChanged != null) {
                CountChanged(this, new EventArgs());
            }
        }

        /// <summary>
        /// Occurs when the counter has completed counting
        /// </summary>
        public event EventHandler CountCompleted;
        private void OnCountCompleted(Task task) {
            if (CountCompleted != null) {
                CountCompleted(this, new EventArgs());
            }
        }

        #endregion

        #region Properties

        private int m_Maximum = 100;
        /// <summary>
        /// Sets the maximum value the counter will count to 
        /// </summary>
        public int Maximum {
            get {
                return m_Maximum;
            }
            set {
                m_Maximum = value;
            }
        }

        #endregion

        #region Methods

        private void Count() {
            for (int i = 0; i < Maximum; i++) {
                Thread.Sleep(50);
            }
        }

        /// <summary>
        /// Runs the counter by starting from 0 and incrementing by one, until the counter reaches its maximum
        /// </summary>
        public void Run() {
            Task.Factory.StartNew(Count).ContinueWith(OnCountCompleted);
        }

        #endregion
    }
}

I shouldn't have to explain the code, but I will say that the main difference you will see in this event model is that OnCountCompleted defines an argument of type Task. This is because we must specify a Action<Task> when calling ContinueWith when we run our task. We don't care about the Task object in the event handler, we just want to notify the event that it was completed, and with this approach having to pass an Action<Task> is just a small side-effect.

Often in development you want to write components like this (not specifically a counter), but essentially a class (or wrapper) that does all the work you need to, and is thread-safe at the same time. The reason we do this is we don't want to have to make it thread-safe everywhere we use it in UI. If I have a class that does not provide thread-safe events, that means I have to write all this thread-safe code in each UI that I use it in. The class we just went over is extremely simple, so let's wire-up the progress.

First, we need to add a TaskScheduler field to the class.

#region Fields

private TaskScheduler m_TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

#endregion

This should be pretty self-explanatory, but in a nutshell we define this at class-scope, and it creates the object on the same thread that the class is created on (of course). So essentially, when you create the Counter class somewhere in your WinForm, the TaskScheduler is created on the same thread as the form: your UI thread.

Next, we need to actually report progress in our Count() method that actually does the counting. The cool thing about the Task Parallel Library is we can do this in a single line of code.

private void Count() {
    for (int i = 0; i &lt; Maximum; i++) {         
        Thread.Sleep(50);         
            Task.Factory.StartNew(() =&gt; OnCountChanged(),
            CancellationToken.None,
            TaskCreationOptions.None,
            m_TaskScheduler)
        .Wait();
    }
}

We are simply starting a new task using the existing TaskFactory, and hooking it up to our method that invokes the event handler. We don't need to specifiy anything special for arguments, but we need to make sure we pass in our TaskScheduler. This is important, because the task scheduler will invoke the task on the thread the task scheduler is on: the UI thread.

The second important thing is that we are calling the Wait method. The reason we are doing this is because we want to give whatever hooks up to the event time to execute their logic. For example, a progress bar to update and paint.

That's all there is too it.. let's use the code in a simple WinForms app with a progress bar and a button.

//-----------------------------------------------------------------------------
// <copyright file="ShellForm.cs" company="DCOM Productions">
//     Copyright (c) DCOM Productions.  All rights reserved.
// </copyright>
//-----------------------------------------------------------------------------

namespace TasksWithThreadSafeEvents.Forms {
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using TasksWithThreadSafeEvents.Objects;

    public partial class ShellForm : Form {
        /// <summary>
        /// Instantiates a new instance of the TasksWithThreadSafeEvents.Forms.ShellForm class
        /// </summary>
        public ShellForm() {
            InitializeComponent();
        }

        private void OnButtonClick(object sender, EventArgs e) {
            Counter counter = new Counter();
            counter.CountChanged += OnCountChanged;
            counter.CountCompleted += OnCountCompleted;
            counter.Maximum = uxProgressBar.Maximum;
            uxProgressBar.Value = 0;
            counter.Run();
        }

        private void OnCountChanged(object sender, EventArgs e) {
            uxProgressBar.Value++;
        }

        private void OnCountCompleted(object sender, EventArgs e) {
            MessageBox.Show("The counter has reached its maximum", "Counter", 
                MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
    }
}

The code looks clean, and it was very little work to implement. You could of course modify the code to actually report a value for progress if you wanted by creating your own class deriving from EventArgs, and passing the for-loop indexer to the event args of the event. I hope this helps those out there looking to write simple, thread-safe classes using the Task Parallel Library.

Download Example Code
TasksWithThreadSafeEvents.zip

5 Comments

  1. Norbs

    Absolutely fantastic!
    I was looking a long time for such an easy to use approach to make my clases async – as easy it is!

    All the best!

  2. Sheir

    Hi,
    I think the link to the download is still broken as I get a 404.

    1. David Anderson (Post author)

      I just noticed I had put two download links on this article. The working link is at the very top, but I will update the non-working link at the bottom as well. Thank you for pointing this error out to me Sheir.

  3. Malcolm Ellis

    Nice… just what I was looking for, but the link to the example zip is dead. Any chance you can fix that? TIA 😉

    1. David Anderson (Post author)

      I updated the link to the proper url. Thank you for pointing that out Malcolm.

Leave a Comment