dotNet Threading, Part IAuthor: Randy MorinDate Added: 17th Mar 2003 Type: Tutorial Rating: This is a printable version of "dotNet Threading, Part I". For the complete online version, please visit http://www.devarticles.com/content.php?articleId=466 Page 1: IntroductionOne of my New Year wishes for this coming year was that the standard committees would agree on threading classes for the C++ language. This limitation of the C++ language standard means that I have to rewrite my threading library each time I start a new job with a new company. I’ve always wished there was a standard threading library that I could use wherever I go. Fortunately, C# does not have this disadvantage. Right from the get go, the language inherits an entire set of threading classes from the dotNet framework. The “System.Threading” dotNet namespaces includes 14 utility classes, 4 exception classes, 2 structures, 6 delegates and 3 enumerations. I’ll present most of these in this article. Page 2: Creating ThreadsCreating a thread in C# is close to trivial, but not quite. The only non-trivial thing about creating a thread is dotNet delegate-classes. Let me explain in few words what is a delegate class. The delegate is a wrapper around a code construct in the dotNet. The code construct could be an object instance, an instance method or a static method. Delegates are used when you want to pass one of the three code constructs as a parameter to another method. When creating a new thread you have to use the ThreadStart delegate class to wrap the instance method that will be executed in the newly created thread. The instance method must return void and must not have any parameters. void ThreadStart() To create a new thread, first create a new ThreadStart object, passing the instance method of the thread procedure in the constructor. The new delegate object is then passed to the constructor of the Thread. Thread thread = new Thread( new ThreadStart(obj.ThreadStart)); You’ve now created a new thread, but the thread is not yet started. To start the thread, you call the Thread.Start instance method. thread.Start(); And that’s it. You have a new running thread. A complete console application that creates a thread and outputs a couple messages to the console window is shown in Listing 1. Listing 1: Creating Threads using System; A nice feature of dotNet threads, and for that matter any dotNet object, is the ability name the object. If you name your threads, then the debugger will pick up those names and you’ll have a much easier time debugging (see Figure 1). Figure 1: Named Threads in Debugger The frame in the bottom left of the IDE window in Figure 1 shows all the threads in out C# application. I set a breakpoint in the PrintHelloFromThread Name static method in Listing 1 and ran the application. When the application stops on the breakpoint, I called up the threads window from the menu bar, Debug | Window | Threads. As you can see, the Name in the threads window of the IDE is the same as the name given the Thread object in our C# code. Page 3: Thread PoolsI was very impressed when I found out that the dotNet framework library included the “System.Threading.ThreadPool” class. I was also impressed by how easy it was to use. You need not create the pool of threads, nor do you have to specify how many consuming threads you require in the pool. The ThreadPool class handles the creation of new threads and the distribution of the wares to consume amongst those threads. You can kick off a consuming thread pool by simply invoking the ThreadPool.QueueUserWorkItem static method. ThreadPool.QueueUserWorkItem( new WaitCallback(Consume), ware); The parameters of the QueueUserWorkItem static method are the WaitCallback delegate that wraps the instance method used in consuming your ware and the ware that you are passing to the method. Your consuming instance method must return void and take one object parameter. The ware that is passed to the QueueUserWorkItem method will be passed into your consuming instance method as the one object parameter. public void Consume(Object obj) Again, the simplicity of C# and the dotNet framework shine through. In just a few lines of code, I’ve recreated a multithreaded consumer-producer application (see Listing 2). Ware For the rest of this article I define a ware to be an item that is produced by the producing thread and consumed by a consuming thread in the consumer-producer design pattern. This is a very narrow definition of the word, but one that suits this article. Listing 2: Creating Thread Pools using System; I added the line Thread.Sleep(100) in the Consume method to simulate the processing that a consumer would normally have performed on the ware. If I didn’t include this Sleep’ing, then one consumer thread could have handled all 100 wares. The additional Sleep’ing forces the dotNet framework to create additional threads and more accurately portrays the features of the ThreadPool class. Page 4: Synchronization Objects, Race Condition, Monitor and LockSynchronization Objects The previous code contains some rather inefficient coding when the main thread cleans up. I repeatedly test the queue length every second until the queue length reaches zero. This may mean that the process will continue executing for up to a full second after the queues are finally drained. Wow! I can’t have that. OK! Maybe that’s not a good reason to change the code, but it is a convenient excuse for me to introduce you to the System.Threading.ManualResetEvent class. Using a ManualResetEvent object, I could trigger the main thread to complete as soon as the last ware was consumed. I’ll do this by creating two new instance data members, a bool WaitForComplete to tell us when the main thread is waiting to exit and a ManualResetEvent Event object that will signal the main thread to exit (see Listing 3). Listing 3: Using Events private bool WaitForComplete; When the consuming thread finishes consuming a ware and detects that the WaitForComplete is true, it will trigger the Event when the queue length is zero. Instead of calling the while block when it wants to exit, the main thread calls the Wait instance method. This method sets the WaitForComplete flag and waits on the Event object. Let me test your threading prowess. The previous listing contained a race condition. Can you find it? Take a minute or two before continuing. Tic! Tic! Tic! Race Condition A race condition is a bug caused by an incorrect assumption as to the timing of two events, that is, that one event would always occur before the other. The race condition occurs when the system shuts down. If the main thread is swapped out in the Wait instance method between testing if the queue length is zero and setting the WaitForComplete flag to true and then the last consuming thread exits the Consume instance method while the main thread is in this state, the event will never be triggered. I ran the code a few hundred times and was never able to trigger the condition. You can’t reproduce it because the main thread should be waiting on the event object well before the last consuming-thread exits. Monitor and Lock I could have arranged the code otherwise to prevent this race condition, but now I’ve created another opportunity to introduce you to the System.Threading.Monitor class and the lock C# construct. The monitor design pattern is most familiar to Java developers. In Java, the synchronized keyword allowed the developer to create quick critical sections within their code. The Java construct was often called a monitor. The dotNet framework presents a similar class called the Monitor that implements traditional wait and signal methods called Wait and Pulse. The C# compiler uses this Monitor class to implement a language construct called a lock. The lock is established on an object and while the lock is established, nobody else can acquire the lock and must wait till the lock is freed. I used this lock construct to prevent our previous race condition (see Listing 4). Listing 4: Using Monitors public void Wait() Preventing the concurrent setting and testing of the queue length and WaitForComplete flag by two different threads removes the race condition. The lock ensures that the setting and testing of these two variables is essentially atomic. Page 5: Join, AutoResetEvent & TimerJoin Before dotNet, I was often asked questions about how to wait for a Win32 thread to exit. The solution was to acquire a handle to the thread and wait on the handle. Or alternatively, you could setup an event that was triggered at the end of the thread and wait on that event. dotNet provides us with a simpler method of doing the same. If you call the Thread.Join instance method, then the current thread will wait until the thread represented by the Thread object is terminated (see Listing 5). Listing 5: Using Join using System; In this previous listing, the main thread creates a new thread (pump), then waits for the thread to complete by calling the pump.Join instance method. If you run the previous code, as is, then the output will be the numbers 0 to 99 and finally the word Goodbye. If you remove the call to pump.Join, then the Goodbye message may be printed before the last number. I chose to put the main thread to sleep for half a second as this displayed the Goodbye message in the middle of the stream of numbers (when pump.Join was removed). AutoResetEvent & Timer Early in the article, I introduced you to the ManualResetEvent class. This class allowed you to set and reset (signal and unsignal) the event by calling the Set and Reset instance methods. The System.Threading.AutoResetEvent class is very similar to the ManualResetEvent class, but when a thread waiting on the event is signaled, the one thread is released and the event is returned to the unsignaled state. This removes the necessity to reset the signal after a thread is signaled. Another great class in the System.Threading namespace is the Timer class. This class allows you to signal an event at a particular interval in time in the future. The Timer class is implemented using a delegate callback instance method. When the Timer is signaled, the class calls the instance method that you specified in the constructor of the Timer object. The Timer callback can also receive a parameter object passed in the call to the Timer constructor. Listing 6: AutoResetEvent and Timer Class using System; Note that the Timer callback instance method is wrapped in a TimerCallback delegate object. The main thread will create an AutoResetEvent object and a Timer object. The main thread then waits on the event object. The TimerCallback instance method is called after one second, triggering the event object. Because the event object is automatically reset, when the main thread attempts to wait on the event again, the thread yields until the event is signaled a second time. The TimerCallback instance method waits another second and then signals the event a second time, releasing the main thread. More In the second part of this article, I will complete my discussion of the synchronization objects and will discuss thread local storage, COM interoperability and thread states. For more great programming articles, please visit http://www.devarticles.com. If you have an opinion or question about this article, then please post it at the devArticles developer forum, which is located at http://www.devarticles.com/forum |
本文地址:http://com.8s8s.com/it/it45718.htm