Posts A hardware monitor in C# 4.0 with the Task Parallel Library - Part 1 - The basics
Post
Cancel

A hardware monitor in C# 4.0 with the Task Parallel Library - Part 1 - The basics

Sometimes you need to continuously and in the background retrieve and process data from an external device connected to your system (for example a GPS receiver or you want to monitor your ethernet connection). The external device has maybe a low level driver/interface which only has the possibility to retrieve its data synchronously. I designed a small framework in C# 4.0 that makes a nice threaded abstraction layer around this. The multithreaded part is implemented with the help of the Task Parallel Library. In this and following posts I will try to describe it step by step. The complete code can be found here and all code snippets below refer to the Part 1 project in the solution.

Disclaimer: I don’t pretend that this is the ONLY and/or MOST EFFICIENT way to implement this. It just works for me. If you have any remarks or suggestions for improvement or bugs, please leave them in the comments below.

Ok, after this introduction, LET’S GET STARTED!

Interface

First we start by defining an interface. Obviously we want to start and stop the retrieval/monitoring and since the monitor will be run on a background thread we define an event that is going to be fired when new data arrives.

1
2
3
4
5
6
7
public interface IDeviceMonitor<TDeviceData> : IDisposable
{
    void Start();
    void Stop();
    event EventHandler<MonitorStatusEventArgs> MonitorStatusChanged;
    event EventHandler<DataReceivedEventArgs<TDeviceData>> DataReceived;
}

The generic parameter TDeviceData defines a class in which you can put the data your hardware/device makes available. An instance of this class is passed along with the event. Since hardware communication may involve access to unmanaged resources we make it IDisposable as well. We also define an event that is fired when the state of the monitor changes (e.g. from started to stopped or vice versa).

Base class

Then we define a base class in which we put all the TPL stuff and other plumbing code so that they are hidden away nicely from the actual implementations. (those implementations contain probably a lot of low level hardware interaction, and we don’t want to get all this plumbing code in the way.)

1
public abstract class DeviceMonitorBase<TDeviceData> : IDeviceMonitor<TDeviceData>

Here we define a couple of lifecycle methods which can (or must) be overridden in a derived class.

1
2
3
4
5
6
7
8
9
// This is called after the monitor is started before entering it's main event loop. You can put optional initialization code in here, like opening ports, setting baudrates and other stuff
protected virtual void Initialize() { }       

// This is called in the main event loop and is the method in which it all happens, reading the data from a device, and must be (obviously) overridden in a derived class
protected abstract TDeviceData ReadData();    //

// This is called after the monitor receives the request to stop. Here you can put clean up code like closing ports, etc. //
protected virtual void CleanUp() { };
public virtual void Dispose() { };

NOTE: It’s not recommended to put here ‘expensive’ operations like freeing memory or freeing device handles, put this code in the Dispose() method. The purpose of this method is to put the monitor in a sort of ‘sleeping’ state so that it can be started again quickly.

The same counts for the Initialize method, don’t put there any memory allocation or other ‘heavy’ operation, since this will be executed every time when the monitor is started. You can put those heavy operations in the constructor of your derived class.

TPL stuff

The whole process is executed in a Task with a continuation task which handles the case when the main task is canceled after a call to the Stop() method. The main task (readerTask) is created in the Start() method and its operations are defined in a lambda expression.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void Start()
{
    this.ctSource = new CancellationTokenSource();
    CancellationToken ct = ctSource.Token;

    this.readerTask = new Task(() =>
    {
         //Are we cancelled yet?
         ct.ThrowIfCancellationRequested();

         //Notify everyone that we started
         OnStatusChanged(MonitorStatus.STARTED);

         //Do initialization work
         Initialize();

         //This is our main loop and reads data until this task is cancelled
         while (true)
         {
             if (ct.IsCancellationRequested)
             {
                 //Cancel it
                 CleanUp();
                 ct.ThrowIfCancellationRequested();
             }

             //Read the data from the device
             TDeviceData data = ReadData();

             //Fire event
             OnDataReceived(data);
         }

    }, ct);

    ...
}

One further remark: In case you don’t already know, but cancellation of a task is accomplished with CancellationTokens that are created from a CancellationTokenSource. In the Stop() method actual cancellation is done by calling the Cancel() method on the CancellationTokenSource from which the CancellationToken is created. In the task an exception will be thrown which actually cancels and stops the task. See here for more info about task cancellation.

The continuation task executes only when its predecessor (our readerTask) has been cancelled. In this case a message is printed to the console, but you can write this message to a log file or event log or whatever. It also notifies all the subscribers that the monitor has stopped

1
2
3
4
5
this.readerTask.ContinueWith(t =>
 {
   Console.WriteLine("Read task stopped");
   OnStatusChanged(MonitorStatus.STOPPED);
 }, TaskContinuationOptions.OnlyOnCanceled);

Example implementation

I made a sample implementation to put the theory from above in practice. You can find it in the TimerMonitor class. It’s just an amazingly unwieldy way to display the current time, but I just wanted to show you a simple example. I created also a sample console application which runs the TimerMonitor

Well, I hope you have learned from this post and found it useful. Stay tuned for Part 2 of this series in which I will add exception handling. You can have a sneak preview when you look at the Part 2 project in the solution.

Happy coding!

Comments

This post is licensed under CC BY 4.0 by the author.