Introduction
This article outlines an alternate method of writing a responsive multithreaded Windows Forms GUI in C#. When I say "alternate," I mean a technique that does not follow the current Microsoft mantra that only the thread that created a GUI control should interact with it. This technique should only be considered when one or more controls in your GUI are processing tens or hundreds of messages a second, causing the GUI to become unresponsive. Typically this is true when using real-time data feeds. The controls in question should be read-only, not updated via the GUI. In all other cases, the standard BeginInvoke
/SycnchronizationContext
/AsyncOperation
calls should be used. For the majority of Windows Forms GUIs, this technique is not appropriate.
Background
Many years ago when I was writing real-time C Windows applications, it was not uncommon to associate several threads with several of the application's child windows in order to improve the responsiveness of the GUI. Over the years, this technique has been used (and documented) less and less. Originally, Microsoft documented and encouraged developers to use this technique. Today, the technique has become totally taboo. It's gotten to the stage now that when I discuss this technique with other .NET developers, they don't even believe it will work. Through this article, I intend to show that in some situations (see Introduction) it is perfectly all right (and safe) to update GUI controls from other threads.
The Sample Code
The sample application is a very basic proof-of-concept skeleton. It contains no validation or exception handling code. Obviously, this would not be the case in production code. The first thing the sample code does is create the threads to be associated with the selected main form's child controls and get them running. This is done in the main form's Load
event handler.
private void theMainForm_Load(object sender, EventArgs e)
{
theMainForm.CheckForIllegalCrossThreadCalls = false;
worker1 = new Thread(new ThreadStart(UpdateListView));
worker1.Name = "Worker1";
worker2 = new Thread(new ThreadStart(UpdateListView));
worker2.Name = "Worker2";
worker3 = new Thread(new ThreadStart(UpdateListView));
worker3.Name = "Worker3";
worker4 = new Thread(new ThreadStart(UpdateListView));
worker4.Name = "Worker4";
Start();
}
Notice in the above code that we set the form's CheckForIllegalCrossThreadCalls
property to false
. This property was added in .NET 2.0. If this was not set to false
, the framework would throw InvalidOperationException
at run-time, as can be seen below:
The UpdateListView
method that is executed by the threads simply fills and then empties its associated list view over and over until it is told to stop. In a more realistic scenario, the thread would probably wait on a synchronization object. When the object is signaled, the thread would probably process any items in a work queue associated with the control. Here's the UpdateListView
method:
private void UpdateListView()
{
ListView lv = null;
ListViewItem item = null;
string name = Thread.CurrentThread.Name;
int loopFor = 20;
int sleepFor = 25;
int count = 0;
switch (name)
{
case "Worker1":
lv = listView1;
break;
case "Worker2":
lv = listView2;
break;
case "Worker3":
lv = listView3;
break;
case "Worker4":
lv = listView4;
break;
}
while (run)
{
for (int i = 0; i < loopFor; ++i)
{
item = new ListViewItem(DateTime.Now.ToString("HH:mm:ss.ffff"));
item.SubItems.Add(string.Format("{0}: item {1}", name, ++count));
lv.Items.Insert(0,item);
Thread.Sleep(sleepFor);
}
for (int i = 0; i < loopFor; ++i)
{
lv.Items.RemoveAt(0);
Thread.Sleep(sleepFor);
}
}
}
Running the Sample Application
When you start the sample application, the four ListView controls will immediately start filling with text and then emptying. While this is happening, try resizing the form. Update the status bar text or display the modal About dialog from the main menu. Drag the ListViews column headers around to re-order them. Click the column headers several times to sort the columns in ascending or descending order. This is all thread-safe.
See how responsive the application feels despite all the activity in the ListView controls. This is due to the fact that all of the heavy work is being done in the background by the four threads associated with the ListView controls. All of the other controls on the form are going through the main GUI thread. Before the application can be closed, the "End Loop" button must be pressed in order to terminate the four threads associated with the four ListView controls.
Performance
Provided that care is taken to enforce that only the thread associated with a particular control is allowed to update its content (either its items collection or data source), this technique can produce an extremely fast and responsive GUI. An additional benefit can be seen in the simplification of the code, as no calls to InvokeRequired()
, BeginInvoke()
or EndInvoke()
are required. In addition, the performance overhead of marshaling calls onto the GUI thread has also been removed. If speed in the Windows Forms GUI is your top priority, you might want to give this technique a try.
Miscellaneous Notes
Please remember that as this is a technique not often seen, so make sure your code is heavily commented to protect the innocent. This code was developed using Visual Studio 2005.
History
- 7 December, 2007 -- Original version posted
- 18 December, 2007 -- Updated sample code and text to clarify my intent