GOTCHA 62 Accessing WinForm controls from arbitrary threads is dangerous


GOTCHA #62 Accessing WinForm controls from arbitrary threads is dangerous

Typically, you execute a task in a thread other than the main thread if that task might take a long time, but meanwhile you want your application to be responsive, plus you want to be able to preempt the task. Once it has completed, how do you display the results in the form's control? Keep in mind that the form's controls are not thread-safe. The only methods and properties of a control that are thread-safe are BeginInvoke(), EndInvoke(), Invoke(), InvokeRequired, and CreateGraphics(). This is not a flaw. It was done by design to improve performance by eliminating thread-synchronization overhead. If you want to access any other methods or properties of a control, you should do so only from the thread that owns the control's underlying window handle. This is typically the thread that created the control, which generally is the main thread in your application.

What happens if you access the non-thread-safe methods of a control from another thread? Unfortunately, the program may appear to work fine on several occasions. But just because the program runs, it does not mean it has no problems. In this case it is not a question of if, but of when the program will misbehave. These kinds of problems are difficult to predict and often may not be easily reproduced.

How can you call a method on a control from within another thread? You can do that using the System.Windows.Forms.Control.Invoke() method on the control. This method executes a delegate on the thread that owns the control's window handle. The call to Invoke() blocks until that method completes.

Say you create a System.Windows.Forms.Timer in a Form. Should you access the controls of a Form from within the Timer's callback method? What about from the callback of an asynchronous method call to a web service? The answers depend on understanding the thread in which each of these executes. Things become clearer when you examine the details.

An example will clarify this. Figure 7-26 shows a WinForm application with three buttons and a couple of labels.

Figure 7-26. A WinForm application to understand threads


The Timer button is linked to code that creates a System.Windows.Forms.Timer. It in turn executes a method, SetExecutingThreadLabel(), that provides some details on the executing thread.

It is important to distinguish this Timer, which resides in the System.Windows.Forms namespace, from the Timer class you've already seen, which is in System.Timers.Timers. (See Gotcha #58, "Threads from the thread pool are scarce" and Gotcha #61, "Exceptions thrown from threads in the pool are lost.")


The Timers Timer button is linked to a handler that creates a System.Timers.Timer object. It raises an event that also executes SetExecutingThreadLabel(). Finally, the Delegate button is tied to a handler that creates a Delegate. It then calls its BeginInvoke(), which also executes SetExecutingThreadLabel(). The code is shown in Examples 7-29 and 7-30.

Example 7-29. Executing thread for different Timers and Delegate (C#)

C# (ControlThread)

         private void Form1_Load(object sender, System.EventArgs e)         {             MainThreadLabel.Text                 = AppDomain.GetCurrentThreadId().ToString();         }         private delegate void SetLabelDelegate(string message);         private void SetExecutingThreadLabel(string message)         {             ExecutingThreadLabel.Text = message;         }         private void TimerButton_Click(             object sender, System.EventArgs e)         {             Timer theTimer = new Timer();             theTimer.Interval = 1000;             theTimer.Tick += new EventHandler(Timer_Tick);             theTimer.Start();         }         private void Timer_Tick(object sender, EventArgs e)         {             Invoke(new SetLabelDelegate(SetExecutingThreadLabel),                 new object[] {                     "Timer : "                     + AppDomain.GetCurrentThreadId() + ": "                     + InvokeRequired });             (sender as Timer).Stop();         }         private void TimersTimerButton_Click(             object sender, System.EventArgs e)         {             System.Timers.Timer theTimer                 = new System.Timers.Timer(1000);             theTimer.Elapsed                 += new System.Timers.ElapsedEventHandler(                     Timer_Elapsed);             theTimer.Start();         }         private void Timer_Elapsed(             object sender, System.Timers.ElapsedEventArgs e)         {             Invoke(new SetLabelDelegate(SetExecutingThreadLabel),                 new object[] {                     "Timers.Timer : "                     + AppDomain.GetCurrentThreadId() + ": "                     + InvokeRequired });             (sender as System.Timers.Timer).Stop();         }         private delegate void AsynchDelegate();         private void DelegateButton_Click(             object sender, System.EventArgs e)         {             AsynchDelegate dlg                 = new AsynchDelegate(AsynchExecuted);             dlg.BeginInvoke(null, null);         }         private void AsynchExecuted()         {             Invoke(new SetLabelDelegate(SetExecutingThreadLabel),                 new object[] {                     "Delegate : "                     + AppDomain.GetCurrentThreadId() + ": "                     + InvokeRequired });         } 

Example 7-30. Executing thread for different Timers and Delegate (VB.NET)

VB.NET (ControlThread)

     Private Sub Form1_Load(ByVal sender As System.Object, _            ByVal e As System.EventArgs) Handles MyBase.Load         MainThreadLabel.Text _             = AppDomain.GetCurrentThreadId().ToString()     End Sub     Private Delegate Sub SetLabelDelegate(ByVal message As String)     Private Sub SetExecutingThreadLabel(ByVal message As String)         ExecutingThreadLabel.Text = message     End Sub     Private Sub TimerButton_Click( _            ByVal sender As System.Object, _            ByVal e As System.EventArgs) _            Handles TimerButton.Click         Dim theTimer As Timer = New Timer         theTimer.Interval = 1000         AddHandler theTimer.Tick, _             New EventHandler(AddressOf Timer_Tick)         theTimer.Start()     End Sub     Private Sub Timer_Tick(ByVal sender As Object, _            ByVal e As EventArgs)         Invoke(New SetLabelDelegate( _             AddressOf SetExecutingThreadLabel), _          New Object() { _           "Timer : " _           & AppDomain.GetCurrentThreadId() & ": " _           & InvokeRequired})         CType(sender, Timer).Stop()     End Sub     Private Sub TimersTimerButton_Click( _            ByVal sender As System.Object, _            ByVal e As System.EventArgs) Handles TimersTimerButton.Click         Dim theTimer As System.Timers.Timer = _             New System.Timers.Timer(1000)         AddHandler theTimer.Elapsed, _             New System.Timers.ElapsedEventHandler( _                 AddressOf Timer_Elapsed)         theTimer.Start()     End Sub     Private Sub Timer_Elapsed( _            ByVal sender As Object, _            ByVal e As System.Timers.ElapsedEventArgs)         Invoke(New SetLabelDelegate( _             AddressOf SetExecutingThreadLabel), _          New Object() { _           "Timers.Timer : " _           & AppDomain.GetCurrentThreadId() & ": " _           & InvokeRequired})         CType(sender, System.Timers.Timer).Stop()     End Sub     Private Delegate Sub AsynchDelegate()     Private Sub DelegateButton_Click(ByVal sender As System.Object, _            ByVal e As System.EventArgs) Handles DelegateButton.Click         Dim dlg As AsynchDelegate _             = New AsynchDelegate(AddressOf AsynchExecuted)         dlg.BeginInvoke(Nothing, Nothing)     End Sub     Private Sub AsynchExecuted()         Invoke(New SetLabelDelegate( _             AddressOf SetExecutingThreadLabel), _         New Object() { _           "Delegate : " _           & AppDomain.GetCurrentThreadId() & ": " _           & InvokeRequired})     End Sub 

Running the program and clicking on the buttons produces the results shown in Figures 7-27 through 7-29.

Figure 7-27. Output from Example 7-29 clicking Timer button


Figure 7-28. Output from Example 7-29 clicking Timers Timer button


Figure 7-29. Output from Example 7-29 clicking Delegate button


A few observations can be made from the above example and the related output:

  • The event executed by the System.Windows.Forms.Timer runs in the main thread. So it is perfectly safe to access the controls directly from within this Timer's event handler. Be careful not to put any long-running code in this method. Otherwise, you will be holding up the main event-dispatch thread, and the performance and responsiveness of your application will suffer.

  • The System.Timers.Timer's event handler executes in a thread from the thread pool. From this thread, you should not interact with the controls directly, as it is not the thread that owns them. You have to use the System.Windows.Forms.Control.Invoke() method.

  • The Delegate's BeginInvoke() method calls the method from a thread pool thread as well. You should not access the controls directly from the invoked method either. Here, too, you have to call Invoke().

If you write an application that involves several threads, you can see how the above details can complicate your efforts. Further, forgetting them may lead to programs that misbehave. How can you ease these concerns?

One good way is to write a method that talks to the controls. Instead of accessing the controls from any random thread, call this method. It can easily check if it's OK to access the controls directly, or if it should go through a delegate.

Let's modify the code in Example 7-29 to illustrate this. From the three event-handler methods, call SetExecutingThreadLabel(). In this method, check to see if the executing thread is the one that owns the control. This can be done using the control's InvokeRequired property.[*]

[*] Thanks to Naresh Chaudhary for refactoring this code.

If the executing thread does own the control (InvokeRequired is false), then access it directly. Otherwise, SetExecutingThreadLabel() uses the Invoke() method to call itself, as shown in Example 7-31.

Example 7-31. Effectively addressing InvokeRequired issue

C# (ControlThread)

         private void SetExecutingThreadLabel(string message)         {             if(ExecutingThreadLabel.InvokeRequired)             {                 Invoke(new SetLabelDelegate(SetExecutingThreadLabel),                     new object[] {message});             }             else             {                 ExecutingThreadLabel.Text = message;             }         }         private void Timer_Tick(object sender, EventArgs e)         {             (sender as Timer).Stop();             SetExecutingThreadLabel(                     "Timer : "                     + AppDomain.GetCurrentThreadId() + ": "                     + InvokeRequired);         }         private void Timer_Elapsed(             object sender, System.Timers.ElapsedEventArgs e)         {             (sender as System.Timers.Timer).Stop();             SetExecutingThreadLabel(                 "Timer : "                 + AppDomain.GetCurrentThreadId() + ": "                 + InvokeRequired);         }         private void AsynchExecuted()         {             SetExecutingThreadLabel(                 "Delegate : "                 + AppDomain.GetCurrentThreadId() + ": "                 + InvokeRequired);         } 

VB.NET (ControlThread)

     Private Sub SetExecutingThreadLabel(ByVal message As String)         If ExecutingThreadLabel.InvokeRequired Then             Invoke(New SetLabelDelegate( _                     AddressOf SetExecutingThreadLabel), _                     New Object() {message})         Else             ExecutingThreadLabel.Text = message         End If     Private Sub Timer_Tick(ByVal sender As Object, _            ByVal e As EventArgs)         CType(sender, Timer).Stop()         SetExecutingThreadLabel( _           "Timer : " _           & AppDomain.GetCurrentThreadId() & ": " _           & InvokeRequired)     End Sub     Private Sub Timer_Elapsed( _            ByVal sender As Object, _            ByVal e As System.Timers.ElapsedEventArgs)         CType(sender, System.Timers.Timer).Stop()         SetExecutingThreadLabel( _           "Timer : " _           & AppDomain.GetCurrentThreadId() & ": " _           & InvokeRequired)     End Sub     Private Sub AsynchExecuted()         SetExecutingThreadLabel( _           "Delegate : " _           & AppDomain.GetCurrentThreadId() & ": " _           & InvokeRequired)     End Sub 

As you can see from this example, the methods executed by the various threads don't have to worry about Invoke(). They can simply call a method (SetExecutingThreadLabel() in this case) to communicate with the control, and that method determines if it has to use Invoke(). This approach not only makes it easier, it also helps deal with situations where you may inadvertently access controls from the wrong thread.

(The output screens are not shown here, because the change in the code does not affect them, except that the thread IDs are different.)

IN A NUTSHELL

Understand which thread is executing your code. This is critical to decide if you can communicate with your controls directly, or if you should use the System.Windows.Forms.Control.Invoke() method instead.

SEE ALSO

Gotcha #58, "Threads from the thread pool are scarce" and Gotcha #61, "Exceptions thrown from threads in the pool are lost."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net