Much of the .NET framework is not considered thread-safe. This means that it isn't safe to invoke operations directly from one thread to a method in a .NET framework class. This is especially true of Windows Forms controls.
However, this doesn't mean you can't use multithreading in Windows Forms applications; it just means you have to be careful when you do. When interacting with the .NET framework, you need to use the synchronous Invoke or asynchronous BeginInvoke and EndInvoke methods to interact with Windows Forms controls. (You can call Invoke , BeginInvoke , EndInvoke , and CreateGraphics across threads safely.)
Listing 6.13 demonstrates how to interact with Windows Forms. The code simulates an animated graphic. By using a thread and a GraphicsPath object, the code draws the yin yang symbol ( ) repeatedly while the graphical user interface is allowed to run.
Listing 6.13 Safely Combining Multithreading with GDI+ in a Windows Forms Application
1: Imports System.Threading 2: Imports System.Drawing 3: Imports System.Drawing.Drawing2D 4: Imports System.Text.RegularExpressions 5: 6: Public Class Form1 7: Inherits System.Windows.Forms.Form 8: 9: [ Windows Form Designer generated code ] 10: 11: Private Sub Form1_Load(ByVal sender As System.Object, _ 12: ByVal e As System.EventArgs) Handles MyBase.Load 13: 14: Start() 15: 16: End Sub 17: 18: Private Thread As Thread 19: Private Sub Start() 20: Thread = New Thread(AddressOf ThreadStart) 21: Thread.IsBackground = True 22: Thread.Start() 23: End Sub 24: 25: Private Sub ThreadStart() 26: While (Thread.CurrentThread.IsAlive) 27: Try 28: Invoke(New MyDelegate(AddressOf Draw)) 29: Catch 30: End Try 31: 32: Thread.CurrentThread.Sleep(50) 33: End While 34: End Sub 35: 36: Private Delegate Sub MyDelegate() 37: 38: Private Sub Draw() 39: Dim Path As GraphicsPath = New GraphicsPath() 40: Path.AddString(Chr(91), New FontFamily("Wingdings"), _ 41: 0, 44, New Point(10, 10), New StringFormat()) 42: 43: Dim Graphics As Graphics = CreateGraphics() 44: Graphics.SmoothingMode = SmoothingMode.AntiAlias 45: 46: Static I As Integer = 0 47: I = (I + 1) Mod (Path.PointCount - 1) 48: Graphics.DrawLine(Pens.Black, Path.PathPoints(I), _ 49: Path.PathPoints(I + 1)) 50: If (I = Path.PointCount - 2) Then Invalidate() 51: 52: End Sub 53: 54: End Class
Line 9 emulates code outlining in Visual Studio .NET. The designer-generated code remained unmodified, so there was no point in showing it. The Form1_Load event starts the multithreaded drawing process in line 14. Start creates an instance of the Thread class, passes the address of the ThreadStart procedure, sets Thread.IsBackground to True , and starts the thread in line 22.
The drawing thread runs in the background until the application is closed. The actual drawing is pushed onto the same thread synchronously using the form's thread-safe Invoke method in line 28. In line 32 the thread sleeps for 50 milliseconds , adding to the animated effect. I used a Try . . . Catch block in lines 27 through 30 because it is possible for the main form to be in the process of being disposed between the time the While check occurs in line 26 and the Invoke method occurs in line 28. Without the exception handler you may intermittently get an ObjectDisposedException when you close the form.
Admittedly, code of the quality and complexity demonstrated in Listing 6.13 is best left for the Tick event of the Timer control. However, it is succinct enough to fit tidily in this chapter and demonstrate the use of the Thread class. Reserve the Thread class in actual applications for when you need maximum control and are willing to take maximum responsibility.
Multithreading with the Timer Class
The basic behavior of a timer is that you provide an interval of time and an event handler representing work; when the interval has elapsed, the event handler is called. Suppose that you are not building a Windows Forms application but want this basic behavior, or that you are in Windows and want a multithreaded version of your basic Timer behavior. You can use the System.Threading.Timer class.
The System.Threading.Timer class is a multithreaded timer. When the wait interval elapses, the delegate is invoked on its own thread, unlike the Timer.Tick event, which occurs on the same thread as the one the Windows Form is on. Besides the fact that the System.Threading.Timer class is not a control, everything else about the threaded Timer is familiar.
To use the System.Threading.Timer class, create an instance of the Timer class, passing an instance of the TimerCallback delegate. Additionally, pass any additional information you need to the state argument, a due time, and a period. The due time and period are expressed in milliseconds. Due time indicates how long to wait before the Timer wakes up, and the period expresses the interval. Listing 6.14 demonstrates how to use the System.Threading.Timer class.
Listing 6.14 Using the Multithreaded Timer Class, Distinct from the Timer Control
1: Imports System.Threading 2: 3: Module Module1 4: 5: Private Finished As Boolean 6: 7: Sub Main() 8: Dim Timer As Timer = _ 9: New Timer(AddressOf TimerCallback, Nothing, 5000, 5000) 10: While (Not Finished) 11: Thread.Sleep(100) 12: End While 13: End Sub 14: 15: Public Sub TimerCallback(ByVal State As Object) 16: Static I As Integer = 0 17: I += 5 18: If (I > 30) Then Finished = True 19: 20: Console.WriteLine(I & " seconds have elapsed") 21: 22: End Sub 23: End Module
The example uses an implicit instance of a delegate (line 9). AddressOf TimerCallback is implicitly converted to a System.Threading.TimerCallback delegate. As expressed by the second argument, Nothing , we aren't passing additional information. The Timer in Listing 6.14 will wait five seconds, invoke the delegate, and then invoke the delegate every additional five seconds.