Manipulating Threads


Threads are manipulated using the class Thread, which can be found in the System.Threading name- space. An instance of Thread represents one thread, or one sequence of execution. You can create another thread by simply instantiating another instance of the Thread object. Let's next look at starting a thread.

To make the following code snippets more concrete, suppose you are writing a graphics image editor, and the user requests to change the color depth of the image. For a large image this can take a while. It's the type of situation where you'd probably create a separate thread to do the processing so that you don't tie up the user interface while the color depth change is happening. To start up a thread, you first need to instantiate a thread object:

 // entryPoint has been declared previously as a delegate // of type ThreadStart Thread depthChangeThread = new Thread(entryPoint); 

Here you have given the variable the name depthChangeThread.

Note

Additional threads that are created within an application in order to perform some task are often known as worker threads.

The preceding code shows that the Thread constructor requires one parameter, which is used to indicate the entry point of the thread — that is, the method at which the thread starts executing. Because you are passing in the details of a method, this is a situation that calls for the use of delegates. In fact, a delegate has already been defined in the System.Threading class. It is called ThreadStart, and its signature looks like this:

 public delegate void ThreadStart(); 

The parameter you pass to the constructor must be a delegate of this type (another way using anonymous methods is shown shortly).

After doing this, however, the new thread isn't actually doing anything yet. It is simply sitting there waiting to be started. You start a thread by calling the Thread.Start() method.

Suppose you have a method, ChangeColorDepth(), which does this processing:

 void ChangeColorDepth() { // processing to change color depth of image } 

You would arrange for this processing to be performed with this code:

 ThreadStart entryPoint = new ThreadStart(ChangeColorDepth); Thread depthChangeThread = new Thread(entryPoint); depthChangeThread.Name = "DepthChange Thread"; depthChangeThread.Start(); 

After this point, two threads are running simultaneously.

In this code, you have also assigned a user-friendly name to the thread using the Thread.Name property (see Figure 13-1). It's not necessary to do this, but it can be useful.

image from book
Figure 13-1

Note that because the thread entry point (ChangeColorDepth() in this example) cannot take any parameters, you will have to find some other means of passing in any information that the method needs. The most obvious way would be to use member fields of the class this method is a member of. In addition to not being able to take any parameters in, the method also cannot return anything. (Where would any return value be returned to? As soon as this method returns a value, the thread that is running it will terminate, so there is nothing around to receive any return value and you can hardly return it to the thread that invoked this thread, because that thread will presumably be busy doing something else.)

The .NET Framework 2.0 introduces some new features that also change the way in which you can start threads. This latest iteration of the .NET Framework introduces anonymous methods that now enable you to avoid creating a separate method and allow you to place the code block of the method directly in the delegate declaration instead. This new addition considerably changes the way in which you can start up your threads (as was earlier shown with the ChangeColorDepth() method).

If you want to use anonymous methods (this option is only available in C# and is not available in Visual Basic), you would alter the ChangeColorDepth() method to the following:

  void ChangeColorDepth() { Thread depthChangeThread = new Thread(delegate() { // processing to change color depth of image }); depthChangeThread.Name = "DepthChange Thread"; depthChangeThread.Start(); } 

As you can see from this example, using anonymous threads provides a cleaner block of code that is also easier to follow. Using anonymous methods also means that there is no need to then employ the ThreadStart delegate.

Once you have started a thread, you can also suspend, resume, or abort it. Suspending a thread means pausing the thread or putting it to sleep — the thread will simply not run for a specific period of time, which also means it will not take up any processor time while it waits. It then can later be resumed, which means it will simply carry on from the point at which it was most recently suspended. If a thread is aborted, it will stop running altogether. Windows will permanently destroy all data that it maintains relating to that thread, so the thread subsequently cannot be restarted after it is aborted.

Continuing with the image editor example, assume that for some reason the user interface thread displays a dialog giving the user a chance to suspend temporarily the conversion process (it is not usual for a user to want to do this, but it is only an example; a more realistic example might be the user pausing the playing of a sound or video file). You code the response like this in the main thread:

 depthChangeThread.Suspend(); 

If the user subsequently asks for the processing to resume, use this method:

 depthChangeThread.Resume(); 

Finally, if the user (more realistically) decides against the conversion after all and chooses to cancel it, you then use the Abort() method:

 depthChangeThread.Abort(); 

It is important to note that the Suspend() and Abort() methods do not necessarily work instantly. In the case of Suspend(), .NET might allow the thread being suspended to execute a few more instructions in order to reach a point at which .NET regards the thread as safely suspendable. This is for technical reasons — really to ensure the correct operation of the garbage collector (for details see the MSDN documentation). In the case of aborting a thread, the Abort() method actually works by throwing a ThreadAbortException in the affected thread. ThreadAbortException is a special exceptionclass that is never handled. This ensures that any associated finally blocks are executed before the thread that is currently executing code inside try blocks is killed. Furthermore, this ensures that any appropriate cleaning up of resources can be done and also gives the thread a chance to make sure that any data it was manipulating (for example, fields of a class instance that will remain around after the thread has been killed) is left in a valid state.

Note

Prior to .NET, aborting a thread in this way was not recommended except in extreme cases because the affected thread simply was killed immediately, which meant that any data it was manipulating could be left in an invalid state, and any resources the thread was using would be left open. The exception mechanism used by .NET in this situation means that aborting threads is safer.

Although this exception mechanism makes aborting a thread safe, it does mean that aborting a thread might actually take some time, because theoretically there is no limit on how long code in a finally block could take to execute. Due to this, after aborting a thread, you might want to wait until the thread has actually been killed before continuing any processing. You would really only wait if any of your subsequent processing relies on the other thread having been killed. You can wait for a thread to terminate by calling the Join() method:

 depthChangeThread.Abort();  depthChangeThread.Join(); 

Join() also has other overloads that allow you to specify a time limit on how long you are prepared to wait. If the time limit is reached, execution will continue anyway. If no time limit is specified, the thread that is waiting will wait for as long as it has to.

The previous code snippets will result in one thread performing actions on another thread (or at least in the case of Join(), waiting for another thread). However, what happens if the main thread wants to perform some actions on itself? In order to do this it needs a reference to a thread object that represents its own thread. It can get such a reference using a static property, CurrentThread, of the Thread class:

 Thread myOwnThread = Thread.CurrentThread; 

Thread is actually a slightly unusual class to manipulate because there is always one thread present even before you instantiate any others — the thread that you are currently executing. This means that you can manipulate the class in two ways:

  • You can instantiate a thread object, which will then represent a running thread and whose instance members apply to that running thread.

  • You can call any of a number of static methods. These generally apply to the thread you are actually calling the method from.

One static method you might want to call is Sleep(). This method puts the running thread to sleep for a set period of time, after which it will continue.

The ThreadPlayaround Sample

To illustrate how to use threads, in this section you build a small sample program called Thread Playaround. The aim of this example is to give you a feel for how manipulating threads works, so it is not intended to illustrate any realistic programming situations.

The core of the ThreadPlayaround sample is a short method, DisplayNumbers(), that counts up to a large number, displaying every so often its current count. DisplayNumbers()starts by displaying the name and culture of the thread that it is being run on:

 static void DisplayNumbers() { Thread thisThread = Thread.CurrentThread; string name = thisThread.Name; Console.WriteLine("Starting thread: " + name); Console.WriteLine(name + ": Current Culture = " + thisThread.CurrentCulture); for (int i=1 ; i<= 8*interval ; i++) { if (i%interval == 0) Console.WriteLine(name + ": count has reached " + i); } } 

The limit of the count depends on interval, a field whose value is typed by the user. If the user types 100, you will count up to 800, displaying the values 100, 200, 300, 400, 500, 600, 700, and 800. If the user types 1000, you will count up to 8000, displaying the values 1000, 2000, 3000, 4000, 5000, 6000, 7000, and 8000 along the way, and so on. This might all seem like a pointless exercise, but the purpose of it is to tie up the processor for a period while allowing you to see how far the processor is progressing with its task.

ThreadPlayaround starts a second worker thread, which will run DisplayNumbers(), but immediately after starting the worker thread, the main thread begins executing the same method. This means that you should see both counts happening at the same time.

The Main() method for ThreadPlayaround and its containing class looks like this:

 class EntryPoint { static int interval; static void Main() { Console.Write("Interval to display results at?> "); interval = int.Parse(Console.ReadLine()); Thread thisThread = Thread.CurrentThread; thisThread.Name = "Main Thread"; ThreadStart workerStart = new ThreadStart(StartMethod); Thread workerThread = new Thread(workerStart); workerThread.Name = "Worker"; workerThread.Start(); DisplayNumbers(); Console.WriteLine("Main Thread Finished"); Console.ReadLine(); }  } 

The start of the class declaration is shown here so that you can see that interval is a static field of this class. In the Main() method, you first ask the user for the interval. Then you retrieve a reference to the thread object that represents the main thread — this is done so that you can give this thread a name and see what's going on in the output.

Next, you create the worker thread, set its name, and start it off by passing it a delegate that indicates that the method it must start in is a method called workerStart. Finally, you call the DisplayNumbers() method to start counting. The entry point for the worker thread is this:

 static void StartMethod() { DisplayNumbers(); Console.WriteLine("Worker Thread Finished"); } 

Note that all these methods are static methods in the same class, EntryPoint. Note also that the two counts take place entirely separately, because the variable i in the DisplayNumbers() method that is used to do the counting is a local variable. Local variables are not only scoped to the method they are defined in but are also visible only to the thread that is executing that method. If another thread starts executing the same method, that thread will get its own copy of the local variables. You start by running the code and selecting a relatively small value of 100 for the interval:

 ThreadPlayaround Interval to display results at?> 100 Starting thread: Main Thread Main Thread: Current Culture = en-US Main Thread: count has reached 100 Main Thread: count has reached 200 Main Thread: count has reached 300 Main Thread: count has reached 400 Main Thread: count has reached 500 Main Thread: count has reached 600 Main Thread: count has reached 700 Main Thread: count has reached 800 Main Thread Finished Starting thread: Worker Worker: Current Culture = en-US Worker: count has reached 100 Worker: count has reached 200 Worker: count has reached 300 Worker: count has reached 400 Worker: count has reached 500 Worker: count has reached 600 Worker: count has reached 700 Worker: count has reached 800 Worker Thread Finished

As far as threads working in parallel are concerned, this doesn't immediately look like it's working too well! You see that the main thread starts, counts up to 800, and then claims to finish. The worker thread then starts and runs through separately.

The problem here is actually that starting a thread is a major process. After instantiating the new thread, the main thread comes across this line of code:

 workerThread.Start(); 

This call to Thread.Start() informs Windows that the new thread is to be started, then immediately returns. While you are counting up to 800, Windows is busily making the arrangements for the thread to be started. This internally means, among other things, allocating various resources for the thread, and performing various security checks. By the time the new thread is actually starting up, the main thread has already finished its work!

You can solve this problem by choosing a larger interval, so that both threads spend longer in the DisplayNumbers() method. Try 1000000 this time:

 ThreadPlayaround Interval to display results at?> 1000000 Starting thread: Main Thread Main Thread: Current Culture = en-US Main Thread: count has reached 1000000 Starting thread: Worker Worker: Current Culture = en-US Main Thread: count has reached 2000000 Worker: count has reached 1000000 Main Thread: count has reached 3000000 Worker: count has reached 2000000 Main Thread: count has reached 4000000 Worker: count has reached 3000000 Main Thread: count has reached 5000000 Main Thread: count has reached 6000000 Worker: count has reached 4000000 Main Thread: count has reached 7000000 Worker: count has reached 5000000 Main Thread: count has reached 8000000 Main Thread Finished Worker: count has reached 6000000 Worker: count has reached 7000000 Worker: count has reached 8000000 Worker Thread Finished

Now you can see the threads really working in parallel. The main thread starts and counts up to one million. At some point, while the main thread is counting the next million numbers, the worker thread starts off, and from then on, the two threads progress at the same rate until they both finish.

It is important to understand that unless you are running a multiprocessor computer, using two threads in a CPU-intensive task will not have saved any time. On a single-processor machine, having both threads count up to 8 million will have taken just as long as having one thread count up to 16 million. Arguably, it will take slightly longer, because with the extra thread around, the operating system has to do a little bit more thread switching, but this difference will be negligible. The advantage of using more than one thread is twofold. First, you gain responsiveness, in that one of the threads could be dealing with user input while the other thread does some work behind the scenes. Second, you will save time if at least one thread is doing something that doesn't involve CPU time, such as waiting for data to be retrieved from the Internet, because the other threads can carry out their processing while the inactive thread(s) are waiting.

Thread Priorities

What happens if you are going to have multiple threads running in your application, but some threads are more important than others? For this, it is possible to assign different priorities to different threads within a process. In general, a thread will not be allocated any time slices if there are any higher-priority threads working. The advantage of this is that you can guarantee user responsiveness by assigning a slightly higher priority to a thread that handles receiving user input. For most of the time, such a thread will have nothing to do, and the other threads can carry on their work. However, if the user does anything, this thread will immediately take priority over other threads in your application for the short time that it spends handling the event.

High-priority threads can completely block threads of lower priority, so you should be careful when changing thread priorities. You should also be aware that thread priorities are treated differently on different operating systems. The thread priorities are defined as values of the ThreadPriority enumeration. The possible values are Highest, AboveNormal, Normal, BelowNormal, and Lowest.

You should note that each process has a base priority, and that these values are relative to the priority of your process. Giving a thread a higher priority might ensure that it gets priority over other threads in that process, but there might still be other processes running on the system whose threads get an even higher priority. Windows tends to give a higher priority to its own operating system threads.

You can see the effect of changing a thread priority by making the following change to the Main() method in the ThreadPlayaround sample:

ThreadStart workerStart = new ThreadStart(StartMethod); Thread workerThread = new Thread(workerStart); workerThread.Name = "Worker"; workerThread.Priority = ThreadPriority.AboveNormal; workerThread.Start();

This indicates that the worker thread should have a slightly higher priority than the main thread. The result is dramatic:

 ThreadPlayaroundWithPriorities Interval to display results at?> 1000000 Starting thread: Main Thread Main Thread: Current Culture = en-US Starting thread: Worker Worker: Current Culture = en-US Main Thread: count has reached 1000000 Worker: count has reached 1000000 Worker: count has reached 2000000 Worker: count has reached 3000000 Worker: count has reached 4000000 Worker: count has reached 5000000 Worker: count has reached 6000000 Worker: count has reached 7000000 Worker: count has reached 8000000 Worker Thread Finished Main Thread: count has reached 2000000 Main Thread: count has reached 3000000 Main Thread: count has reached 4000000 Main Thread: count has reached 5000000 Main Thread: count has reached 6000000 Main Thread: count has reached 7000000 Main Thread: count has reached 8000000 Main Thread Finished

This shows that when the worker thread has an AboveNormal priority, the main thread scarcely gets a look-in once the worker thread has started.

Synchronization

One crucial aspect of working with threads is the synchronization of access to any variables that more than one thread has access to. Synchronization means that only one thread should be able to access the variable at any one time. If you do not ensure that access to variables is synchronized, subtle bugs can result. This section briefly reviews some of the main issues involved.

What is synchronization?

The issue of synchronization arises because what looks like a single statement in your C# source code in most cases will translate into many statements in the final compiled assembly language machine code. Take, for example, the following statement:

 message += ", there"; // message variable is a string that contains "Hello" 

This statement looks syntactically in C# like one statement, but it actually involves a large number of operations when the code is being executed. Memory will need to be allocated to store the new longer string; the variable message will need to be set to refer to the new memory; the actual text will need to be copied, and so on.

Obviously, the case is exaggerated here by selecting a string — one of the more complex data types — as an example, but even when performing arithmetic operations on primitive numeric types, there is quite often more going on behind the scenes than you would imagine from looking at the C# code. In particular, many operations cannot be carried out directly on variables stored in memory locations, and their values have to be separately copied into special locations in the processor known as registers.

In a situation where a single C# statement translates into more than one native machine code command, it is quite possible that the thread's time slice might end in the middle of executing that statement process. If this happens, another thread in the same process might be given a time slice, and, if access to variables involved with that statement (here: message) is not synchronized, this other thread might attempt to read or write to the same variables. In the example, was the other thread intended to see the new value of message or the old value?

The problems can get even worse than this. The statement used in the example is relatively simple, but in a more complicated statement, some variable might have an undefined value for a brief period, while the statement is being executed. If another thread attempts to read that value in that instant, it might simply read garbage. More seriously, if two threads simultaneously try to write data to the same variable, it is almost certain that that variable will contain an incorrect value afterward.

Synchronization is not an issue that affects the ThreadPlayaround sample, because both threads use mostly local variables. The only variable that both threads have access to is the interval field, but this field is initialized by the main thread before any other thread starts, and subsequently only reads from either thread, so there is still not a problem. Synchronization issues only arise if at least one thread is writing to a variable while other threads are either reading or writing to it.

Fortunately, C# provides an extremely easy way of synchronizing access to variables, and the C# language keyword that does it is lock. You use lock like this:

 lock (x) { DoSomething(); } 

What the lock statement does is wrap an object known as a mutual exclusion lock, or mutex, around the variable in the round brackets. The mutex will remain in place while the compound statement attached to the lock keyword is executed. While the mutex is wrapped around a variable, no other thread is permitted access to that variable. You can see this with the preceding code; the compound statement will execute, and eventually this thread will lose its time slice. If the next thread to gain the time slice attempts to access the variable x, access to the variable will be denied. Instead, Windows will simply put the thread to sleep until the mutex has been released.

The Mutex object does allow you to lock an object from another thread (thereby blocking the thread) regardless of where that thread originates. The Mutex object allows for locking of objects whether the second thread is contained within the same process or is originating in an entirely different process. Therefore, the Mutex object is a system-wide solution to thread synchronization.

The mutex is the simplest of a number of mechanisms that can be used to control access to variables. Wedon't have the space to go into the others here, but they are all controlled through the .NET base class System.Threading.Monitor. In fact, the C# lock statement is simply a C# syntax wrapper around a couple of method calls to this class.

In general, you should synchronize variables wherever there is a risk that any thread might try writing to a variable at the same time as other threads are trying to read from or write to the same variable. There is not space here to cover the details of thread synchronization; it is a fairly big topic in its own right. The following sections are confined to pointing out a couple of the potential pitfalls.

Synchronization issues

Synchronizing threads is vital in multithreaded applications. However, it's an area in which it is important to proceed carefully because a number of subtle and hard-to-detect bugs can easily arise, in particular deadlocks and race conditions.

Don't overuse synchronization

Although thread synchronization is important, it is important to use it only where it is necessary because it can impact performance for two reasons. First, there is some overhead associated with actually putting a lock on an object and taking it off, though this is admittedly minimal. Second, and more importantly, the more thread synchronization you have, the more threads can get held up waiting for objects to be released. Remember that if one thread holds a lock on any object, any other thread that needs to access that object will simply halt execution until the lock is released. It is important, therefore, that you place as little code inside lock blocks as you can without causing thread synchronization bugs. In this sense, you can think of lock statements as temporarily disabling the multithreading ability of an application, and therefore temporarily removing all the benefits of multithreading.

However, the dangers of using synchronization too often (performance and responsiveness go down) are not as great as the dangers associated with not using synchronization when you need it (subtle runtime bugs that are very hard to track down).

Deadlocks

A deadlock (or a deadly embrace) is a bug that can occur when two threads have to access resources that are locked by the other. Suppose one thread is running the following code, where a and b are two object references that both threads have access to:

 lock (a) { // do something lock (b) { // do something } } 

At the same time, another thread is running this code:

 lock (b) { // do something lock (a) { // do something } } 

Depending on the times that the threads come across the various statements, the following scenario is quite possible: the first thread acquires a lock on a, while at about the same time the second thread acquires a lock on b. A short time later, the first thread comes across the lock(b) statement, and immediately goes to sleep, waiting for the lock on b to be released. Soon afterward, the second thread comes across its lock(a) statement and also puts itself to sleep, ready for Windows to wake it up the instant the lock on a gets released. Unfortunately, the lock on a is never going to be released because the first thread, which owns this lock, is sleeping and won't wake up until the lock on b gets released, which won't happen until the second thread wakes up. The result is deadlock. Both threads just permanently sit there doing nothing, each waiting for the other thread to release its lock. This kind of problem can cause an entire application to just hang, so that you have no choice but to use the Task Manager to terminate the entire process.

Note

In this situation, it is not possible for another thread to release the locks; a mutual exclusion lock can only be released by the thread that claims the lock in the first place.

Deadlocks can usually be avoided by having both threads claim locks on objects in the same order. In the preceding example, if the second thread claimed the locks in the same order as the first thread, a first, then b, whichever thread has the lock on a first would completely finish its task and then the other thread would start. This way, no deadlock can occur.

You might think that it is easy to avoid coding deadlocks — after all, in the example, it looks fairly obvious that a deadlock could occur so you probably wouldn't write that code in the first place. However, remember that different locks can occur in different method calls. With this example, the first thread might actually be executing this code:

 lock (a) { // do bits of processing  CallSomeMethod()  } 

Here, CallSomeMethod() might call other methods, and so on, and buried in there somewhere is a lock(b) statement. In this situation, it might not be nearly so obvious when you write your code that you are allowing a possible deadlock.

Race conditions

A race condition is somewhat subtler than a deadlock. It rarely halts execution of a process, but it can lead to data corruption. It is hard to give a precise definition of a race, but it generally occurs when several threads attempt to access the same data, and do not adequately take account of what the other threads are doing. Race conditions are best understood using an example.

Suppose you have an array of objects, where each element in the array needs to be processed somehow, and you have a number of threads that are between them doing this processing. You might have an object, for example, ArrayController, which contains the array of objects as well as an int that indicates how many of them have been processed, and therefore, which one should be processed next. ArrayController might implement this method:

 public int GetObject(int index) { // returns the object at the given index. } 

It also implements this read/write property:

 public int ObjectsProcessed { // indicates how many of the objects have been processed. } 

Now, each thread that is helping to process the objects might execute some code that looks like this:

 lock(ArrayController) { int nextIndex = ArrayController.ObjectsProcessed; Console.WriteLine("Object to be processed next is " + nextIndex); ++ArrayController.ObjectsProcessed; object next = ArrayController.GetObject(nextIndex); } ProcessObject(next); 

This by itself should work, but suppose that in an attempt to avoid tying up resources for longer than necessary, you decide not to hold the lock on ArrayController while you're displaying the user message. Therefore, you rewrite the preceding code like this:

 lock(ArrayController) { int nextIndex = ArrayController.ObjectsProcessed; } Console.WriteLine("Object to be processed next is " + nextIndex); lock(ArrayController) { ++ArrayController.ObjectsProcessed; object next = ArrayController.GetObject(nextIndex); } ProcessObject(next); 

Here, you have a possible problem. What could happen is that one thread gets an object (say, the 11th object in the array), and displays the message saying that it is about to process this object. Meanwhile, a second thread also starts executing the same code, calls ObjectsProcessed, and determines that the next object to be processed is the 11th object, because the first thread hasn't yet updated ArrayController. ObjectsProcessed. While the second thread is happily writing to the console that it will now process the 11th object, the first thread acquires another lock on the ArrayController and inside this lock increments ObjectsProcessed. Unfortunately, it is too late. Both threads are now committed to processing the same object — a textbook example of a race condition.

For both deadlocks and race conditions, it is not often obvious when the condition can occur; and when it does, it is hard to identify the bug. In general, this is an area where you largely learn from experience. However, it is important to consider very carefully all the parts of the code where you need synchronization when you are writing multithreaded applications to check whether there is any possibility of deadlocks or race conditions arising. Keep in mind that you cannot predict the exact times that different threads will encounter different instructions.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

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