Friday, August 8, 2008

Threading with .NET ThreadPool Part 3

To wrap up this series of articles, I am going to make the application we've been working on do something a little more interesting. If you will recall in my first article on the subject of ThreadPool, I go over several ways to call the primary thread pool function QueueUserWorkItem and demonstrate how simple it is to multi-thread an application. In my second article, I look at a skeletal application that doesn't really do anything; it just displays messages on the console. However, and more importantly, it demonstrates some of the aspects of synchronization. You must synchronize if you are to do anything of real consequence with threads.

So, what sort of interesting things will this application do? My application will paint a pop-art picture. Actually, it will place random pixels on a PictureBox. Okay, its not that useful (or argueably that interesting), but it will demonstrate a means by which threads report progress to the GUI thread.

Let's look at the work item classes again and consider my updates. First, there are the changes to "WorkItem". I've added a public event handler called "Done" that will be fired by both of the derived classes. I've also added two public static properties called "Width" and "Height" which simply serve as a convenient place to store some global parameters for the work items to use. As a bonus, I've added some code to paint a background message that should materialize as the dots are painted.


public abstract class WorkItem
{
public abstract void ThreadPoolCallback(object context);
public event EventHandler Done;
public static int Width { get; set; }
public static int Height { get; set; }

private static bool _interrupted = false;
private static long _numWorkItems = 0;
protected static Bitmap _secretMessage;
protected static void InitMessage()
{
_secretMessage = new Bitmap(Width, Height);
Graphics g = Graphics.FromImage(_secretMessage);
g.FillRectangle(Brushes.White, 0, 0, Width, Height);
g.DrawString("Far Out!", new Font("Courier New",(float)18.0), Brushes.Black, (Width / 4), (Height / 2) - 15);
}

protected WorkItem()
{
Interlocked.Increment(ref WorkItem._numWorkItems);
}
protected void WorkItemDone()
{
Interlocked.Decrement(ref WorkItem._numWorkItems);
}
public static bool Interrupted
{
get { return _interrupted; }
set { _interrupted = value; }
}

public long NumWorkItems
{
get { return _numWorkItems; }
}

protected void OnDone()
{
if (Done != null)
Done(this, new EventArgs());
}
}


Next, the MasterWorkItem is modified to provide its "context" variable to each new ServiceWorkItems in the call to QueueUserWorkItem(). The "context" is declared as an object and is designed to let the programmer pass any reference he likes. We will be passing a reference to the GUI form. Furthermore, MasterWorkItem will call OnDone when completing normally to signal the Done event. The number of active work items has been increased to 1000. This increases the speed at which the painting is displayed. But, if you increase it too much, your CPU(s) will saturate and your GUI may not display the image (for me this was between 10-50 thousand work items. The painting will continually update until the GUI interrupts the master work item.


public class MasterWorkItem : WorkItem
{
public override void ThreadPoolCallback(object context)
{
try
{
WorkItem.InitMessage();
while (!Interrupted)
{
if (NumWorkItems < 1000)
{
ThreadPool.QueueUserWorkItem(new ServiceWorkItem().ThreadPoolCallback, context);
}
else
{
Console.WriteLine("Letting pool drain some");
Thread.Sleep(500);
}
}
while (NumWorkItems > 1)
{
Console.WriteLine("Waiting for child tasks {0}", NumWorkItems);
Thread.Sleep(1000);
}
Console.WriteLine("All tasks have completed.");
OnDone();
}
finally
{
WorkItemDone();
}
}
}


The ServiceWorkItem gets a new static method called "MyRand()" and a private static Random object. All the service threads use the same random number generator to determine the location and color of their pixel. Each service thread determines its pixel location and color and "tells" the GUI to paint it. Locks are used around structures that are not safe. Each work item calls OnDone when complete (but we don't implement a handler for it, so it goes ignored). Since threads cannot interact directly with the GUI, the service work item calls a GUI function that tests whether Invoke is required. BeginInvoke() is therefore our primary means of communication back to the GUI of our progress.


public class ServiceWorkItem : WorkItem
{
public override void ThreadPoolCallback(object context)
{
try
{
if (!Interrupted)
{
int X = ServiceWorkItem.MyRand(WorkItem.Width);
int Y = ServiceWorkItem.MyRand(WorkItem.Height);
int R = ServiceWorkItem.MyRand(255);
int G = ServiceWorkItem.MyRand(255);
int B = ServiceWorkItem.MyRand(255);
if (X > ((WorkItem.Width / 2) / 2)
&& X < ((WorkItem.Width / 2) / 2 + (WorkItem.Width / 2))
&& Y > ((WorkItem.Height / 2) / 2)
&& Y < ((WorkItem.Height / 2) / 2 + (WorkItem.Height / 2)))
{
R += 50; R = (R > 255) ? 255 : R;
//B += 50; B %= 256;
}
lock (WorkItem._secretMessage)
{
if (!WorkItem._secretMessage.GetPixel(X, Y).Name.Equals("ffffffff"))
{
R = 255;
B = 255;
G = 255;
}
}
Color clr = Color.FromArgb(R, G, B);

int myThread = Thread.CurrentThread.GetHashCode();
((Form1)context).SetPixelCallback(X,Y,clr);
}
else
{
Console.WriteLine("Thread {0}, WorkItem {1}, Interrupted",
Thread.CurrentThread.GetHashCode().ToString(),
this.GetHashCode().ToString());
}
OnDone();
}
finally
{
WorkItemDone();
}
}
private static Random rnd = new Random();
public static int MyRand(int limit)
{
return rnd.Next(0, limit);
}
}


Finally, the main form (Form1) is changed to provide a Bitmap, a function to update the bitmap, and a PictureBox to display it. The Form1_Load handler initializes the bitmap, paints it white and hooks it up to the pictureBox1. The Load handler also initializes the work item globals, creates a new master work item, wires up the work item's "Done" handler, and starts the master thread.


public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private Bitmap bitmap;
private Random rnd;

void m_Done(object sender, EventArgs e)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new MethodInvoker(() => this.label1.Text = "Done"));
}
else
{
this.label1.Text = "Done";
}
}
private void button1_Click(object sender, EventArgs e)
{
WorkItem.Interrupted = true;
}

private void Form1_Load(object sender, EventArgs e)
{
bitmap = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Graphics g = Graphics.FromImage(bitmap);
g.FillRectangle(Brushes.White, 0, 0, bitmap.Width, bitmap.Height);
pictureBox1.Image = bitmap;

rnd = new Random();

WorkItem.Width = bitmap.Width;
WorkItem.Height = bitmap.Height;
MasterWorkItem m = new MasterWorkItem();
m.Done += new EventHandler(m_Done);
ThreadPool.QueueUserWorkItem(m.ThreadPoolCallback,this);
}

internal delegate void SetPixelDelegate(int x, int y, Color clr);
internal void SetPixelCallback(int X, int Y, Color clr)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new SetPixelDelegate(SetPixelCallback), new object[] { X, Y, clr } );
}
else
{
bitmap.SetPixel(X, Y, clr);
}
}
private void timer1_Tick(object sender, EventArgs e)
{
pictureBox1.Image = bitmap;
}
}


And, here is the code for Form1's code-behind (Form.Designer.cs)...


partial class Form1
{
private System.ComponentModel.IContainer components = null;

protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.button1 = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.timer1 = new System.Windows.Forms.Timer(this.components);
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.SuspendLayout();
//
// button1
//
this.button1.Location = new System.Drawing.Point(29, 217);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 26);
this.button1.TabIndex = 0;
this.button1.Text = "button1";
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(124, 224);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(35, 13);
this.label1.TabIndex = 1;
this.label1.Text = "label1";
//
// pictureBox1
//
this.pictureBox1.Location = new System.Drawing.Point(25, 28);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(239, 162);
this.pictureBox1.TabIndex = 2;
this.pictureBox1.TabStop = false;
//
// timer1
//
this.timer1.Enabled = true;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(292, 266);
this.Controls.Add(this.pictureBox1);
this.Controls.Add(this.label1);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Form1";
this.Load += new System.EventHandler(this.Form1_Load);
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();

}


private System.Windows.Forms.Button button1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Timer timer1;
}


I call my namespace "ThreadPoolApp" and here are my "using" statements as well...


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 System.Threading;

namespace ThreadPoolApp
{
//...
}


To review, there are several aspects of a multi-threaded that in my opinion should be present to be a robust and stable app. These are...

1. Enqueuing work.
2. Detecting queue overload and throttling.
3. Detecting work start.
4. Reporting work progress.
5. Detecting work completion.
6. Stopping work-in-progress.
7. Cancelling un-started work.

But, I've done nothing in my code to detect the starting of work (#3). I leave it to the reader as an exercise, but for something this simple, I would probably use an Invoke to change a visible label since the startup is obvious by observing the updating of the picture. Of course, others may have their own list, and depending upon the application, I may not implement all of these suggestions myself.

To wrap up, let's consider what kind of applications benefit from such an architecture. An application benefits where one or a few threads need to listen for incoming network requests and then pass off a (short lived) work item for processing while it goes back to listening. If you are considering writing a loop for a thread to wait for work and effectively do the same thing or kind of things over and over, your application could probably benefit from this approach. There are several architectural choices when it comes to multi-threading, and that choice is yours. I hope these articles can help your deliberations.

No comments: