Click here to Skip to main content
16,011,508 members
Articles / Desktop Programming / Windows Forms
Article

An Alternate Way of Writing a Multithreaded GUI in C#

Rate me:
Please Sign up or sign in to vote.
1.76/5 (36 votes)
18 Dec 2007CPOL4 min read 107.6K   1.1K   37   26
An article on writing a responsive multithreaded GUI, but not the Microsoft way
Screenshot - GUIThreads1.JPG

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.

C#
private void theMainForm_Load(object sender, EventArgs e)
{
    // Prevent the framework from checking what thread the GUI is updated from.

    theMainForm.CheckForIllegalCrossThreadCalls = false;

    // Create our worker threads and name them.

    // The name will be used to associate a thread with a specific ListView
    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";

    // Get all the threads running.

    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:

Screenshot - GUIThreads2.JPG

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:

C#
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;
    }

    // Keep running until we're told to stop.
    while (run)
    {
        // Add n items to the list.
        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);
        }

        // Now remove them.
        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Oliguy Limited
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralBackground Worker Pin
Angelo Cresta18-Dec-07 22:37
professionalAngelo Cresta18-Dec-07 22:37 
QuestionCan it get any simpler than that? Pin
Grommel10-Dec-07 3:00
Grommel10-Dec-07 3:00 
AnswerRe: Can it get any simpler than that? [modified] Pin
Steve Messer18-Dec-07 7:42
Steve Messer18-Dec-07 7:42 
AnswerRe: Can it get any simpler than that? Pin
Keith Balaam18-Dec-07 9:23
Keith Balaam18-Dec-07 9:23 
GeneralDoesn't always work Pin
vtweasel10-Dec-07 2:11
vtweasel10-Dec-07 2:11 
Your technique does work for the simplest of non-ActiveX controls. For more sophisticated controls, especially those that are ActiveX, it will not work. Sometimes the failure will be spectacular, but more often just puzzling and intermittent.

My old employer had a very large application that was written in the mid-90s to call UI functions from any thread. We had all kinds of strange issues; the most infamous of which we called blackout. After running for some indeterminate period of time, one of our widgets would render itself as completely black. After that point, any control that the user interacted with would suffer blackout until the entire app was just a black rectangle. We solved that problem by painstakingly converting EVERY UI call to the UI thread.

I urge *everyone* who reads this article to refrain from using this "technique". You *will* be sorry unless your app is a simply toy as presented here. If you use grid or tree controls, for example, they'll probably fail (or just not work) much sooner than the simplest, oldest (i.e. buttons, listboxes, editboxes) Windows controls. But fail they will.

.NET makes it **SO EASY** to do a SynchronizeInvoke that you really have no excuse for not doing things the right way.

Having said all of that, are you aware that you can have multiple "UI" threads in a single app? All a "UI" thread is is one with a message pump. You get a pump simply buy calling the GetMessage, TranslateMessage, DispatchMessage Win32 API functions. In old-school unmanaged apps, it is fairly easy to create multiple UI threads. You can then create a child window in a secondary thread and parent it to a window created in the main thread. This actually works. Messages for the child window get handled in the non-Main thread, while parent messages get handled in the main thread. I've even done this in .NET 2.0.

However, this doesn't work for all UI controls, especially the DataGrid. I believe this is because the DataGrid is an ActiveX control under the hood, and it can only live in the default single-threaded apartment.

In summary, this article does tell you a stupid pet trick that you can do with simple Windows controls. Just because you can do something doesn't mean you should. It will bite you. Guaranteed.
QuestionRe: Doesn't always work Pin
pasflug111-Nov-09 7:18
pasflug111-Nov-09 7:18 
GeneralIt is a possible way.. but like everything it has it's place Pin
Jared Allen9-Dec-07 18:43
Jared Allen9-Dec-07 18:43 
GeneralRe: It is a possible way.. but like everything it has it's place Pin
Keith Balaam18-Dec-07 9:12
Keith Balaam18-Dec-07 9:12 
GeneralRe: It is a possible way.. but like everything it has it's place Pin
Jared Allen18-Dec-07 9:24
Jared Allen18-Dec-07 9:24 
GeneralNice Article! Pin
Herman Schoenfeld9-Dec-07 12:43
Herman Schoenfeld9-Dec-07 12:43 
GeneralLazy coding Pin
Pete O'Hanlon9-Dec-07 10:06
mvePete O'Hanlon9-Dec-07 10:06 
GeneralRe: Lazy coding Pin
Keith Balaam18-Dec-07 9:03
Keith Balaam18-Dec-07 9:03 
GeneralUmmm....yeah Pin
Dave Kreskowiak9-Dec-07 8:52
mveDave Kreskowiak9-Dec-07 8:52 
GeneralRe: Ummm....yeah Pin
Keith Balaam18-Dec-07 8:53
Keith Balaam18-Dec-07 8:53 
GeneralNot good at all Pin
Sacha Barber8-Dec-07 21:38
Sacha Barber8-Dec-07 21:38 
GeneralRe: Not good at all Pin
Keith Balaam18-Dec-07 8:48
Keith Balaam18-Dec-07 8:48 
Generalstate machines [modified] Pin
Tomaž Štih7-Dec-07 19:05
Tomaž Štih7-Dec-07 19:05 
QuestionThought this was common knowledge? Pin
Dankarmy7-Dec-07 10:31
Dankarmy7-Dec-07 10:31 
GeneralWait a minute.... Pin
Marc Clifton7-Dec-07 10:26
mvaMarc Clifton7-Dec-07 10:26 
GeneralBad technique Pin
peterchen7-Dec-07 10:18
peterchen7-Dec-07 10:18 
GeneralRe: Bad technique Pin
Keith Balaam18-Dec-07 8:15
Keith Balaam18-Dec-07 8:15 
GeneralWhy many of us consider this bad Pin
peterchen18-Dec-07 14:36
peterchen18-Dec-07 14:36 
GeneralNot thread-safe Pin
Nathan Evans7-Dec-07 10:06
Nathan Evans7-Dec-07 10:06 
AnswerRe: Not thread-safe Pin
Keith Balaam18-Dec-07 7:52
Keith Balaam18-Dec-07 7:52 
GeneralGreat finding Pin
Roberto Colnaghi7-Dec-07 9:11
Roberto Colnaghi7-Dec-07 9:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.