Using threads can give you great power, but as the old saying goes, with great power comes great responsibility. Using threads irresponsibly can easily lead to poor performance and unstable applications. However, using good techniques with threads can produce programs with higher efficiency and more responsive interfaces.
Thankfully, much research has been done in recent years in an attempt to simplify multithreading. In particular, the Task Parallel Library, now a part of .NET 4, allows you to easily split your code execution onto multiple processors without any messing around with threads or locks.
Solution: Use the Parallel
class in the System.Threading
namespace to assign tasks to be automatically scheduled and wait for them to complete. The Parallel
class automatically scales to the number of processors.
When you have a set of data that can be split over multiple processors and processed independently, you can use constructs such as Parallel.For(), as in this example with computing prime numbers:
The output on my dual-core machine is shown here:
Aside from the drastically reduced time, notice that the output results aren’t necessarily in order and that the parallel version of the program doesn’t come up with the same results as the iterative version. Rather than going through sequentially from 1 to 20,000,000 until a million primes are found, the input space is divided up; thus, it’s possible to get results you otherwise wouldn’t. It really depends on your problem and how you divide it up.
Rather than split up the data, you can split up the tasks to work on the data. Suppose you have iterative code that looks like this:
You can use Parallel
with some lambda expressions to parallelize the two calculation methods:
On my system, the results are not as impressive as the previous example, but they are still significant:
Solution: Before relying on thread-aware data structures, you should make sure that you need them. For example, if data doesn’t really need to be accessed on multiple threads, then don’t use them. Similarly, if data is read-only, then it doesn’t need to be protected. Keep in mind that synchronizing thread access to a data structure, although it may be fast, is never as fast as no synchronization at all.
If every access to the data structure needs to be protected, use the concurrent collections described in the first section of Chapter 10, “Collections.”
If only some access needs to be protected, consider using some of the synchronization objects (such as locks) described later in this chapter.
Solution: This is one of the simplest ways of taking advantage of multithreading, as shown next:
This program produces the following output:
You can call EndInvoke
at any time to wait until the operation is done and get the results back.
Solution: Using the thread pool is easier and more efficient than creating your own threads. All you have to do is pass it a method of your choosing.
The BitmapSorter program in the included sample code uses the ThreadPool
to do its sorting in a separate thread. Because the method it wants to call does not match the WaitCallback
delegate signature, it uses an anonymous delegate to simplify the code:
ThreadPool.QueueUserWorkItem( () => {scrambledBitmap.Sort();} );
Solution: If your thread is going to stick around for a while, it’s probably worth it for the application to own it rather than relying on the threadpool (which expects threads to eventually be returned to it).
This program has the following partial output (it will keep running into you press a key):
You should try to use the thread pool as often as possible, especially if your threads are not long-lived. Thread creation is a relatively expensive process, and the pool maintains threads after you’re done using them so that you don’t have the overhead of creating and destroying them continually. The thread pool grows to adapt to your program’s threading needs.
Solution: There are basically two ways of giving a thread access to shared data:
• Pass in object
argument to the thread function.
• Use member fields in the same class.
For an example of the first option, here’s a modified version of the previous code example, with the modified lines bolded:
Because thread functions are just methods in classes, they also get access to all the fields and functionality in that same class (option 2). However, now you have to be careful. As soon as you talk about two threads potentially accessing the same data, you have to think about protecting the data. That’s the topic of the next section.
It is generally a good idea if two threads do not simultaneously try to write to the same area of memory, or even for one to read as the other writes—the results are understandably unpredictable.
Solution: .NET provides the Monitor
class to protect data from access via multiple threads.
Because Monitor.Enter()
and Monitor.Exit()
take any object for the lock, you might be tempted to just pass this
to the methods. In a sense you’re right—this is how it was originally intended. However, this ends up being a bad idea because you can’t control who else locks on your object. Instead, the best practice is just to create a private dummy object
in your class to serve as an explicit lock object.
There is a shortcoming to the preceding code. What if an exception is thrown between Enter
and Exit
? That value will be inaccessible until the process exits. The answer is to wrap it in a try/finally
statement (see Chapter 4, “Exceptions”), and C# has a shorthand notation for this because it’s so common:
Suppose you want to get a lock if possible, but if not, you want to do something else. This is possible, if you drop back to using the Monitor
class explicitly:
You can also pass a TimeSpan
to TryEnter
to tell it to try for that long before giving up.
Locks are all well and good, but understand something: Having lots of threads contending for locks stops your program dead in its tracks. Performance-wise, locks can be expensive in many cases, and even when they’re not, they can still be deadly when performance is critical. The real trick to building highly scalable applications is to avoid using locks as much as possible. This usually means being fiendishly clever in your design from the outset.
Solution: Looking at a preceding example,
you might wonder if a lock is really necessary just to increment a variable. After all, increment and decrement are extremely common operations and locks can be expensive.
Thankfully, there are a few functions to atomically perform some simple operations for you:
This type of functionality is used to implement the rest of the synchronization objects, but you can use them yourself to do simple modifications of fields, or to exchange object references (perhaps you need to swap one data structure for a newer version of it).
Solution: You can use a mutex, which is like a Monitor
object, but at an operating system level. Mutexes must be named so that both processes can open the same mutex object.
Here’s a sample program that uses a mutex to protect a file from being written to by two instances of this same program (see Figure 23.1):
As with monitors, you can attempt a wait on a mutex for a TimeSpan
before giving up. In that case, if the mutex gives up, WaitOne
will return false
.
Make sure you choose good names for your mutexes. Either use a unique combination of your company, application, and mutex purpose, or use a globally unique identifier (GUID) to ensure no collisions with your own apps or others.
Solution: Now that you have a way of creating a cross-process communication mechanism (simple, though it may be), you can use it to prevent users from running more than one instance of our program.
See the SingleInstanceApp demo in the projects for this chapter for the full source code.
If you want your mutex to prevent other users from starting your application, such as in a terminal service environment, prefix your mutex’s names with “Global”. Otherwise, users in different sessions will be able to also run your application simultaneously (which is probably what you want).
Solution: There are many instances where it’s okay to have multiple threads access the same data, but you want to limit it to a certain number. For example, if your application downloads images from the Internet and you want to limit your bandwidth usage, it would be useful to limit the concurrent downloads to two or so. Semaphores can do this for you. In the following example, three progress bars are shown. Three threads are spun up to increment them. However, a Semaphore is created to limit the updating to only two progress bars at a time. This gives you a visual idea of what is happening (see Figure 23.2).
In this case, the resource being protected is GUI updates, but it could be a data structure as in the other examples. See the SemaphoreDemo in the sample projects to see the code in action.
Solution: Events are some of the most useful signaling mechanisms available. There are two types of events: those with a manual reset, and those that automatically reset.
Setting an event is what causes other threads to wake up and continue with their work. When a ManualResetEvent
is set, all waiting threads are woken up and the event remains set until you manually reset it.
Setting an AutoResetEvent
causes only a single thread to be woken up (arbitrarily chosen, from the program’s point of view) and the event is immediately reset.
See the EventDemo project in the accompanying source code for an example that effectively shows this difference. Like the Semaphore demo, the resource being protected is progress bar updating. Figure 23.3 shows this demo in action. Here is a portion of the main source code:
Solution: You’ve seen a few uses for timers so far. Windows Forms has a timer that fires on the UI thread. WPF has the similar DispatcherTimer
.
Another type of timer calls a method using the ThreadPool
discussed earlier. This is good for things that need to happen periodically in your app that don’t necessarily need UI.
Solution: It’s safe to let multiple threads read the data at the same time, but when a thread needs to write, all other threads need to be blocked..NET originally provided the ReaderWriterLock
for this situation, but it has performance problems that outweigh its usefulness in many situations. Thankfully, there is ReaderWriterLockSlim
, which corrects many of its predecessor’s shortcomings.
This program demonstrates using a ReaderWriterLockSlim
on an array that’s shared among a single writer and three readers:
Here’s a portion of the output:
Notice that threads 11 and 12 were able to get a read lock at the same time.
On many I/O classes, you will find a common set of methods. For example, on streams you can call Read
or BeginRead
. The first is a synchronous method that does not return until the data has been read. The second returns immediately, letting your program continue while the file is read from disk, the network, the Internet, or another data source.
Solution: The TextTokenizer sample app, shown in Figure 23.4, demonstrates how to use this method of programming. If only synchronous methods were used, it would be impossible to interact with the program (even just to close it!), but using asynchronous methods allows your program to respond to users and perform other tasks while the I/O is completing.
Although asynchronous programming is quite useful for I/O, you can implement it yourself for any type of operation, such as in the following class which performs the word counting: