« BizTalk 2006 - 2004 Performance Comparison | Main | BizTalk - String manipulation using Business Rules Engine »

Cross Thread UI control access

We all know that when working with Winform applications and threads, we need to be careful about accessing the UI controls. These controls are meant to be accessed only on the thread that built them, which in this case is the primary application thread.

In multithreaded applications, when additional threads are used to perform background jobs to ensure that the UI remains responsive, one has to be careful about updating the controls on the form. There are articles already, that talk about how to address this problem. Check here.

With .net framework 2.0 and VS2005, there are improvements that help work with this issue easily. One of the improvements is the more user friendly exception display that occurs when running the application in debug mode. If your code does cross thread access, it will result in a error message like the following

CrossThreadError.jpg

.Net 2.0 also introduced a new class called BackgroundWorker class. This class has support for doing a lengthy work in background and then posting the completion event to the primary thread. This way in the completion event handler, you can safely update the UI controls without worry of cross thread access.

The article that I refered to earlier, talks about possibilities of using Invoke or BeginInvoke, where we know that the BeginInvoke method does the same work, but asynchronously. One can additionally call this.InvokeRequired to check if we really need to make the Invoke call or the execution is already on the primary thread. With my background in having done lot of Windows programming and working with DispatchMessage and TranslateMessage APIs and also writing message handlers in WndProc, I knew that when BeginInvoke or Invoke is called, it is bound to be doing a PostMessage to the primary thread, but I wanted to really how this was being done.

Needless to say that Lutz Reoder's .Net Reflector came in handy. Without going into line by line details of the internal workings, I will capture the key points here and you are welcome to drill down deeper into this on your own.

The Invoke/BeginInvoke methods support two overloads. One, that takes only a delegate (for callback and actual execution of UI updation logic) and another that takes the delegate as well an object array that you may want to pass to the delegate. The definition of the delegate is really upto the programmer based on the logic required to update the UI controls. The single parameter overloads call the second overload internally by passing null for the second parameter. For example

public IAsyncResult BeginInvoke(Delegate method)
{
      return this.BeginInvoke(method, null);
}

These methods then internally use the MultithreadSafeCallScope internal private class to ensure that the further execution happens in a thread safe way. Eventually the call is made to the control's MarshaledInvoke private method. The method signature is

private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)

The Delegate and the object array parameters are the ones that have been sent from the calling code. The synchronous boolean is set based on if the call is make from Invoke (true) or BeginInvoke (false). The key to this message execution is the check on the current executing thread ID and the thread ID of the control's creation thread. The calls are made using another internal class called SafeNativeMethods. The method calls of interest here are

1. SafeNativeMethods.GetCurrentThreadID : This is a method found in kernel32.dll and as the name suggests, gets the ID of the thread on which the code is currently executing.

2. SafeNativeMethods.GetWindowThreadProcessID : This is a method found in User32.dll and is used to find the thread that created the Window (the Winform in current context) and optionally the process ID also.

You can check Windows SDK documentation for more details on these methods.

The comparison of the thread IDs returned by these methods essentially decides the fate of further execution. If the two values are equal, this means that the current call is already happening on a thread that created the UI and hence can be used to safely modify the UI controls, else, it means that this is a different thread.

A new ThreadMethodEntry is created with the parameters passed to the Invoke/BeginInvoke methods and also capturing the current execution context. This entry is added to the ThreadCallbackList. If we are already on the right thread, a call is made to executed the delegate that the calling code had passed, else, a call to UnSafeNativeMethods.PostMessage is done to post the message to the thread's queue. People with windows programming experience will know the significance of PostMessage or SendMessage Windows APIs.

Finally, if the call came in via Invoke method, i.e. the synchronous parameter is true, the code waits for completion of execution using WaitForWaitHandle, else if called via BeginInvoke, the call completes and the IAsynchResult is returned to the calling code. Since the intention is to update the UI controls via the methods registered using the delegate, most likely you aren't bothered about the IAsynchResult result and in time the call to the delegate will complete, when the primary threads gets around to process its message queue.

To conclude, following are two ways to use the Invoke/BeginInvoke methods. In the code below, I have shown using BeginInvoke, but the same can be replaced with Invoke to get synchronous execution.

Option 1 : Using this.InvokeRequired. This is done when the same method updates the UI controls so need to ensure if we are on the right thread.

        private void btnProcess_Click(object sender, EventArgs e)
        {
            //queue the work for thread processing
            ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc));
        }

        private void ThreadProc(object stateInfo)
        {
            //do some thread specific work here..
            //simulate long work by sleeping for 2 secs
            Thread.Sleep(2000);

            //done. Update UI
            SetTextOnControls()
        }

        private delegate void UICallerDelegate();

        private void SetTextOnControls()
        {
            if (this.InvokeRequired)
            {
                UICallerDelegate dlg = new UICallerDelegate(SetTextOnControls);
                BeginInvoke(dlg, null);
            }
            else
            {
                label1.Text = DateTime.Now.ToShortTimeString();
                label2.Text = DateTime.Now.ToShortTimeString();
                button1.Text = DateTime.Now.ToShortTimeString();
                textBox1.Text = DateTime.Now.ToShortTimeString();
            }
        }

Option 2 : Without using this.InvokeRequired.

        private void btnProcess_Click(object sender, EventArgs e)
        {
            //queue the work for thread processing
            ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc));
        }

        private void ThreadProc(object stateInfo)
        {
            //do some thread specific work here..
            //simulate long work by sleeping for 2 secs
            Thread.Sleep(2000);

            //done. Update UI
            UICallerDelegate dlg = new UICallerDelegate(SetTextOnControls);
            BeginInvoke(dlg, null);
        }

        private delegate void UICallerDelegate();

        private void SetTextOnControls()
        {
            label1.Text = DateTime.Now.ToShortTimeString();
            label2.Text = DateTime.Now.ToShortTimeString();
            button1.Text = DateTime.Now.ToShortTimeString();
            textBox1.Text = DateTime.Now.ToShortTimeString();
        }

Comments

Thanks for helping

The threads from ThreadPool, Delegate.async method invocation and BackgroundWorker have a problem. I.e we cannot set the ApartmentState(can be set only when thread is created). ApartmentState to STA is a necessity sometimes while dealing with com components. Anyway around this?

Rajesh, you will have to then go for regular Threads and create one on your own with new Thread and then set the ApartmentState on it.

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)

Please key in the two words you see in the box to validate your identity as an authentic user and reduce spam.

Subscribe to this blog's feed

Follow us on

Blogger Profiles

Infosys on Twitter