- Found this tutorial by accident? Start with the first C# tutorial.
This is the first tutorial of several on multi-threading. It's applicable to desktop computing, not the web which uses similar but slightly different technologies and uses .NET 4.0 or later technologies so Visual Studio 2010/Visual C# Express 2010 .
- Download BackgroundWorker example source code (zip)
- New to C#? Read About C#
Everyone who programs is familiar with the idea of a thread of execution. In console programs the program runs, starts executing and when complete it finishes. That's a useful model for writing simple programs that do tasks, for example utility programs.
In GUI programs, it's similar but event based. You click a button and an event handler runs. It does something, perhaps updates another control, or loads a file into memory from disk, but again it's a single threaded execution.
Most people have an idea of what multi-threading is. You start a GUI program and its functionality does something that takes time. An example might be a utility that reads files and folders on a hard disk and displays them. To process a folder and recurse through all the sub-folders is a possible candidate for multi-threading. As it does the processing, it should not freeze the main GUI. This is where things start to get a little more complicated.
Windows the GUI is based on lots of windows, basically everything you see is a window. Every control you see on a winform is a window and has a message loop that processes a queue of messages. Messages are sent between windows for clicks, repaint instructions, mouse clicks etc. The GUI uses one thread and if you do anything that takes a long time in this thread then your GUI may freeze and that looks bad.
So the idea is to offload the work to another thread and leave the GUI thread responsive. When it gets a little more complicated is that that other thread must not directly update the GUI. Remember this rule:
- Only the main GUI thread should be used to update the GUI. Never directly from another thread.
Multi-Threading Without Threads
The .NET framework has several technologies that let you avoid explicit threading, we'll start with BackgroundWorker and I'll spend the rest of this tutorial explaining how to use it. Using this class will probably solve most of your "avoid freezing the GUI" problems. It's easy to do and there aren't too many gotchas.
The program will do a recursive descent of a folder, folder by folder and update the GUI as it finds folders and files. It'll populate a ListView with the folder path and name, number of files and total size of files in that folder.
This will be done by the BackgroundWorker which will report progress back so the ListView can be updated.
Working With BackgroundWorkers
The idea is you drop a BackgroundWorker component on the form, setup a DoWork() event handler and run it with the RunWorkerAsync() method of the component. Another event handler RunWorkerCompleted() handles things when the work is completed. This is where exceptions are picked up as well;
So far so good but what about tracking progress? This has to be done through the ProgressChanged() event handler. It is forbidden to directly do any GUI updates in the DoWork() handler that will give you all sorts of grief.
Walking a Folder Tree in C#
There are at least three ways to do this, in a single function call (but that can fail and return nothing) recursively or non recursively using Stack<T>. All are documented on the MSDN article How to Iterate Through a Directory Tree.
In this code I've used the simplest method but there is a gotcha. It will fail on any folder that you don't have permission to read so don't run it against your C:\ drive.
On my Windows 7 C:\ drive there are several folders that my default login can't read. One subfolder has 996 folders beneath it and the program runs in about 2 seconds. This code below at the start of DoWork() fetches all the folders below that.
var bwargs = (List
var folder = new DirectoryInfo(bwargs);
var directories = folder.GetDirectories("*", SearchOption.AllDirectories).ToList();
The e.argument is from the DoWorkEventArgs e and has a single List item which is the path to the starting folder. That's setup with this code in the StartClick eventhandler:
var args = new List
The staring folder is converted into a DirectoryInfo instance and calling GetDirectories on that returns the full list. For each folder in that list, folder.GetFiles() returns a list of files and this information populates the ProgressData class instance.
public class ProgressData
public int FileCount;
public long FileSize;
public string FolderName;
So every folder under the starting folder has a line generated from the foldername and information about the files. I use a LINQ query to sum up the file sizes from the list of FileInfo records returned by folder.GetFiles().
- Want to Learn LINQ? Start with the first LINQ Tutorial
This call at the end of the loop in the DoWork() event handler is what makes it possible to pass the ProgessData instance back and display it on the ListView.
As there were nearly 1,000 folders I used a floating point number to calculate the progressPercent. For each folder the value of progressSlice is added and the integer value is passed in the ReportProgress call along with the progressData instance.
Behind the scenes, the ReportProgress call marshalls this data from the background worker thread and supplies it to the main GUI thread in the ProgressChanged handler. This is a safe way for data to be passed from non GUI threads to the GUI thread.
The ProgressChanged can then fetch the ProgressData using as to change the e.userstate value. If this is null, which happens at the start just ignore it. Then the data is added to the ListView. I do this inside BeginUpdate... EndUpdate for speed. Without it the ListView would try to update itself with every line added.
The 1 MS delay in the line
Is because this thread hogs the CPU. By allowing even such a short delay, it lets other threads run. In particular it lets the Cancel button have a chance to be clicked. This calls:
Which sets a flag that the other thread can read. This flag is BackgroundWorker.CancellationPending and your code in the DoWork should check it periodically. If set then set the DoWorkEventArgs.Cancel flag to true and exit. This flag is read in the RunWorkerCompleted event handler in the RunWorkerCompletedEventArgs.Cancelled flag variable.
- For more things to do with C#, see How to Do Things in C#.