Click here to Skip to main content
Licence CPOL
First Posted 6 Dec 2007
Views 49,751
Downloads 342
Bookmarked 36 times

An Alternate Way of Writing a Multithreaded GUI in C#

By | 18 Dec 2007 | Article
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.

        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:

 
        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)

About the Author

Keith Balaam

Software Developer
Oliguy Limited
United Kingdom United Kingdom

Member



Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Layout  Per page   
  Refresh
GeneralBackground Worker PinmemberAngelo Cresta22:37 18 Dec '07  
QuestionCan it get any simpler than that? PinmemberGrommel3:00 10 Dec '07  
AnswerRe: Can it get any simpler than that? [modified] Pinmembersmesser7:42 18 Dec '07  
AnswerRe: Can it get any simpler than that? PinmemberKeith Balaam9:23 18 Dec '07  
GeneralDoesn't always work Pinmembervtweasel2:11 10 Dec '07  
QuestionRe: Doesn't always work Pinmemberpasflug17:18 11 Nov '09  
GeneralIt is a possible way.. but like everything it has it's place PinmemberJared Allen18:43 9 Dec '07  
GeneralRe: It is a possible way.. but like everything it has it's place PinmemberKeith Balaam9:12 18 Dec '07  
GeneralRe: It is a possible way.. but like everything it has it's place PinmemberJared Allen9:24 18 Dec '07  
GeneralNice Article! Pinmembercxx12:43 9 Dec '07  
GeneralLazy coding PinmemberPete O'Hanlon10:06 9 Dec '07  
GeneralRe: Lazy coding PinmemberKeith Balaam9:03 18 Dec '07  
GeneralUmmm....yeah PinmvpDave Kreskowiak8:52 9 Dec '07  
GeneralRe: Ummm....yeah PinmemberKeith Balaam8:53 18 Dec '07  
GeneralNot good at all PinmemberSacha Barber21:38 8 Dec '07  
GeneralRe: Not good at all PinmemberKeith Balaam8:48 18 Dec '07  
Generalstate machines [modified] PinmemberTomaž Štih19:05 7 Dec '07  
QuestionThought this was common knowledge? PinmemberDankarmy10:31 7 Dec '07  
GeneralWait a minute.... PinprotectorMarc Clifton10:26 7 Dec '07  
GeneralBad technique Pinmemberpeterchen10:18 7 Dec '07  
GeneralRe: Bad technique PinmemberKeith Balaam8:15 18 Dec '07  
GeneralWhy many of us consider this bad Pinmemberpeterchen14:36 18 Dec '07  
Thank you very much for updating the article. Not everyone would have done that after the many discouraging replies.
 
I still wouldn't use the technique in production code. The simplest reason is maintenance: any update, even a patch, for the .NET runtime may "legally" break your code.
 
A slightly deeper reason is that it's not a general technique. Any change to the UI layer may require a different emchanism to work reliably. The third reason is that I know how hard threading issues are to debug, and they might occur only on the computers of your most important customer - and as soon as you add some traces or - behold - run it under the debugger, they are gone.
 
But as Jared said, everything has it's place. For a quick hack, and understanding it's limitations, why not? I just want to avoid some novice programmer who does not understand the details says "cool - that's so easy! And it got a good rating, so it must be right!"
 
Why is this working most of the time, and why sometimes not
 
As said, there is some state of the control that may be altered in a non-threadsafe manner. To understand why this usually doesn't happen, we have to look at some details that are not really documented, and may change with every minor update. And I might be wrong, much of this is derived from guided guessing.
 
We have to look separately at the Win32 control (ListView Control) and the .NET framework class that wraps it. Since the Win32 List View state can only be modified through windows messages (Send/PostMessage), which always execute on the thread that created the control, these changes are threadsafe, though only on a per-message basis*. State that is held directly in the .NET class, however, is not threadsafe.
 
Many of the functions and properties exposed by the wrapper classes sirectly manipulate the Win32 control's state. Most controls can largely be wrapped without implementing any state in the .NET wrapper at all. As long as you are hitting only these properties, usually nothing will go wrong.
 
Even if only one thread modifies .NET state, you are fine. Only when both threads start to modify state that is implemented in the .NET wrapper class - like cell color for the ListView control - you will get into trouble.
 
To be frank, if there would be any errors due to race conditions in your sample, one wouldn't be able to see them with the rapid and seemingly-random updates.
 
Most multithreaded Win32 GUI programming relied on the fact that SendMessage/PostMessage do an implicit thread synchronization. MFC strongly suffered from the problem, and went at great lengths (and quite some hacks) to avoid any state inside the MFC wrapper class.
 
So there are two general solutions for the library designer: don't allow any state in the wrapper class - which pretty much prohibits almost every extension to the control - or make the wrapper class state thread safe, which opens some nasty worm cans.
 
Alternatives
There's no panacea, at least none that doesn't need some fine tuning to the situation, data and control at hand (so my "not a general solution" argument above is a bit weak).
 
The general approach that comes to (my) mind: update the main thread in chunks. Either use BeginInvoke on a block of items, or use a thread safe deque and an update/notification mechanism that fits the scenario. For listview controls, "Virtual Mode" offers can be a boon, though you have to make sure the data store does not become a lock hotspot.


 
*) consider the WM_SETTEXT, WM_GETTEXTLENGTH and WM_GETTEXT messages to get and set control text. Imagine the following scenario:
 
Thread 1 sets control text to "Hamlet".
Thread 2 wants to read the control text. For this it first gets the text length (6 characters),
Thread 2 allocates a buffer for 6 chars + one terminating zero
Thread 1 sets control text to "Ophelia"
Thread 2 sends WM_GETTEXT to get the text into the buffer just allocated.

 
In the best case, thread 2 just gets "Opheli". Many similar APIs have a good chance for a buffer overrun if the programmer isn't extra careful.
 
Everything is still fine if thread 2 is the thread that created the control, and is currently processing a message (because then, Thread one couldn't "inject" a message). If thread 2 is your worker thread, however...
 



We are a big screwed up dysfunctional psychotic happy family - some more screwed up, others more happy, but everybody's psychotic joint venture definition of CP
My first real C# project | Linkify!|">FoldWithUs! | sighist


GeneralNot thread-safe PinmemberNathan Evans10:06 7 Dec '07  
AnswerRe: Not thread-safe PinmemberKeith Balaam7:52 18 Dec '07  
GeneralGreat finding PinmemberRoberto 'Obi-Wan' Colnaghi Junior9:11 7 Dec '07  

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

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.5.120529.1 | Last Updated 19 Dec 2007
Article Copyright 2007 by Keith Balaam
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid