Chapter 23. Threads, Asynchronous, and Parallel Programming

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.

Easily Split Work Among Processors

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.

Process Data in Parallel

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:

image

image

image

image

The output on my dual-core machine is shown here:

image

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.

Run Tasks in Parallel

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:

image

You can use Parallel with some lambda expressions to parallelize the two calculation methods:

image

On my system, the results are not as impressive as the previous example, but they are still significant:

image

Use Data Structures in Multiple Threads

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.

Call a Method Asynchronously

Solution: This is one of the simplest ways of taking advantage of multithreading, as shown next:

image

image

This program produces the following output:

image

You can call EndInvoke at any time to wait until the operation is done and get the results back.

Use the Thread Pool

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.

image

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();} );

Create a Thread

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).

image

This program has the following partial output (it will keep running into you press a key):

image

Note

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.

Exchange Data with a Thread

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:

image

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.

Protect Data Used in Multiple Threads

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.

image

Note

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:

image

Attempt to Gain a Lock

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:

image

You can also pass a TimeSpan to TryEnter to tell it to try for that long before giving up.

Note

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.

Use Interlocked Methods Instead of Locks

Solution: Looking at a preceding example,

image

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:

image

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).

Protect Data in Multiple Processes

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):

image

Figure 23.1 Each process locks the mutex before writing to the file.

image

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.

Note

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.

Limit Applications to a Single Instance

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.

image

See the SingleInstanceApp demo in the projects for this chapter for the full source code.

Note

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).

Limit the Number of Threads That Can Access a Resource

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).

image

image

image

Figure 23.2 When you run this demo you’ll notice that even though three tasks want to perform work, only two can run simultaneously because of the semaphore.

image

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.

Signal Threads with Events

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:

image

image

image

image

Figure 23.3 In manual mode, setting the event will cause all three threads to progress until you click the Reset button. In auto mode, setting the event will cause a single thread to go (until it checks for the event again).

image

Use a Multithreaded Timer

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.

image

Use a Reader-Writer Lock

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:

image

image

Here’s a portion of the output:

image

Notice that threads 11 and 12 were able to get a read lock at the same time.

Use the Asynchronous Programming Model

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.

Figure 23.4 The TextTokenizer app demonstrates asynchronous file reading as well as implementing asynchronous BeginXXX methods on your own class.

image

image

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:

image

image

image

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset