DiskDiff: Populating on and Interrupting a Thread

1. Populating on a Thread

To make your application behave, you need to do the scan on a different thread so the user- interface thread can continue operating. In this example, you’ll use the Thread object from the System.Threading namespace. Starting the thread is easy:

public void Populate()

{

Thread t = new Thread(new ThreadStart(DoPopulate));

t.Start();

}

The function that will be called at the start of the thread is DoPopulate(). To create a new thread, a ThreadStart delegate must be created on the function you want called and passed to the thread. Then, the Start() member on the thread is called, and the thread starts and runs on its merry way.

That gets the process working, but your app is now broken. When the DoTree() function in the form calls Populate(), it will start the thread and return immediately and then try to repaint the tree form. This is bad, because the information isn’t ready to paint yet.

To fix this, you’ll add a new event to the DirectoryNode object for when the populate function is done:

void DoPopulate()

{

DoPopulate(this);

OnPopulateComplete();

}

Because the delegate method doesn’t have much code, you can convert it to an anonymous method and place it with the thread’s creation:

public void Populate()

{

cancelled = false;

Thread t = new Thread(delegate() {

DoPopulate(this);

OnPopulateComplete(true);

});

t.Start();

}

Finally, add a function to the form to repaint the tree when the population is done:

void DoTreeDone(object sender, EventArgs e)

{

Console.WriteLine(“DoTreeDone”);

statusBar1.Text = “”;

treeView1.Nodes.Clear();

PopulateTree(treeView1.Nodes, directoryNode);

}

And, to finish, you’ll hook this function up to the event.

Now, the user interface is still active while the population is happening, so you can move the application around and have it paint correctly.

At least, that’s what you hope will happen, but you have a little problem. The thread doing the population isn’t allowed to do anything that updates the control, and adding a node to the node collection automatically updates the tree view. When you try to do this, the system throws an exception that tells you the update can’t be done on the current thread.

Very nicely, however, the exception that’s thrown tells you exactly what to do; you need to use Control.Invoke() to pass a delegate to the function you want to call, and Control.Invoke() will find the control and arrange to have the function called on the proper thread.

This is actually pretty easy to set up. The first step is to declare a delegate to the function you want to call:

delegate int AddDelegate(TreeNode treeNode);

This delegate matches the signature of the function you want to call. The next step is to modify the call. Instead of this:

treeNodeCollection.Add(treeNode);

you need the following:

AddDelegate addDelegate = new AddDelegate(treeNodeCollection.Add);

treeView1.Invoke(addDelegate, new object[] {treeNode});

The first line sets up the pointer to the function, and the second one passes it off to the control to be called, along with an array of parameters to pass to the function.

With that change, the program starts working again, but if you point it at a big drive, it might take a long time to complete, and you have no way to interrupt it.

2. Interrupting a Thread

You’ll modify the DirectoryNode class to add a CancelPopulate() member function. This function will set an internal flag that the populate code will poll, and the code will terminate if it finds that the flag is set. Because the flag is being modified without thread locking, the volatile keyword has been applied to the flag to let the runtime know it should check for the most current value of this variable rather than using a copy cached on a particular thread.

When doing polling, how often you poll usually comes with a trade-off. If the polling takes place too often, it can take up more time than the processing. Conversely, if the polling takes place rarely, it can make the cancel appear to have no effect. In this case, the polling takes place before processing each file in a directory.

Another option is for the DirectoryNode to store the thread instance and then stop the thread directly by calling the Abort() method. This has less overhead since there’s no polling, but it makes the code a bit less obvious,1 and it also could make cleanup a bit more complex since the object could be left in a bad state when the thread stops. To properly do the cleanup, the processing loop wants to catch the ThreadAbortException that will be thrown when Abort() is called.

Modifying Populate() is simple; the file-processing loop simply tests the flag and aborts the thread if the flag is set:

foreach (FileInfo f in directory.GetFiles())

{

if (rootDirectoryNode.cancelled)

{

Thread.CurrentThread.Abort();

}

rootDirectoryNode.OnFileScanned(f.Name);

this.files.Add(new FileNode(f));

}

This will interrupt the processing, but unfortunately the user interface never finds out the operation was interrupted,[1] [2] so it doesn’t have the opportunity to clear the status bar. You can easily fix this by slightly modifying the PopulateComplete event so the delegate takes a success parameter (so the user interface can do different things in the success and cancel cases), and the call when the processing is canceled becomes the following:

rootDirectoryNode.OnPopulateComplete(false);

Thread.CurrentThread.Abort();

A Cancel Button

To keep things simple, this example will use a button to cancel the processing. Using the designer, you can add a Cancel button at the lower-left corner of the form on top of the tree view. So that it isn’t in the way, set the visibility to false.

When processing is started, the visibility is set to true, and the button is now visible and selectable. The event handler that’s associated with completing the population will set the visibility back to false when the processing is completed or aborted.

Figure 36-1 shows a view of the application while processing a big directory tree.

You now have an application that can be interrupted. In this case, using threads was a simple way to get what you wanted. Another way to do this is to use the asynchronous call mechanism in the .NET CLR (see Chapter 31).

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

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