Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1

.pdf
Скачиваний:
71
Добавлен:
16.08.2013
Размер:
24.18 Mб
Скачать

678 C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

What you lose in control you get back in ease of use. Plus, it simplifies multithreaded programming, especially if your application is made up of numerous threads. With thread pooling, you’re able to focus on developing your business logic without getting bogged down with thread management.

For those of you who are interested, this is, at a high level, how a thread pool works. Basically, a thread pool is created the first time ThreadPool is called. Thread pools use a queuing system that places a work item (a thread request) on an available thread pool thread. If no thread pool thread is available, then a new one is created up to a default maximum of 25 threads per available processor. (You can change this maximum using CorSetMaxThreads, defined in the mscoree.h file.) If the maximum number of threads is reached, then the work item remains on a queue until a thread pool thread becomes available. There is no limit to the number of work items that can be queued. (Well, that’s not quite true. You are restricted to available memory.)

Each thread pool thread runs at the default priority and can’t be cancelled.

Note Thread pool threads are background threads. As such, you need the main program thread or some other foreground thread to remain alive the entire life of the application.

You add a work item to the thread pool queue by calling the ThreadPool class’s static

QueueUserWorkItem() method. The QueueUserWorkItem() method takes a WaitCallback delegate

as a parameter and an Object handle parameter to allow you to pass information to the generated thread. (The method is overloaded so that you don’t have to pass an Object parameter if none is required.) The WaitCallback delegate has the following signature:

public delegate void WaitCallback(Object^ state);

The Object^ state parameter will contain the Object handle that was passed as the second parameter to the QueueUserWorkItem() method. The QueueUserWorkItem() method returns true if the method successfully queues the work item; otherwise, it returns false.

The example in Listing 16-6 shows how simple it is to create two ThreadPool threads.

Listing 16-6. Using Thread Pools

using namespace System;

using namespace System::Threading;

ref class MyThread

{

public:

void ThreadFunc(Object^ stateInfo);

};

void MyThread::ThreadFunc(Object^ stateInfo)

{

for (int i = 0; i < 10; i++)

{

Console::WriteLine("{0} {1}", stateInfo, i.ToString()); Thread::Sleep(100);

}

}

C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

679

void main()

{

Console::WriteLine("Main Program Starts");

MyThread ^myThr1 = gcnew MyThread();

ThreadPool::QueueUserWorkItem(

gcnew WaitCallback(myThr1, &MyThread::ThreadFunc), "Thread1");

ThreadPool::QueueUserWorkItem(

gcnew WaitCallback(myThr1, &MyThread::ThreadFunc), "Thread2");

Thread::Sleep(2000); Console::WriteLine("Main Program Ends");

}

There are only a couple of things of note in the preceding example. The first is the second parameter in the call to the QueueUserWorkItem() method. This parameter is actually extremely flexible, as you can pass it any managed data type supported by the .NET Framework. In the preceding example, I passed a String, but you could pass it an instance to an extremely large and complex class if you want.

The second thing of note is the Sleep() method used to keep the main thread alive. Once the main thread dies, so do all the threads in the ThreadPool, no matter what they are doing.

You can see the results of ThreadPooling.exe in Figure 16-7.

Figure 16-7. The ThreadPooling program in action

Synchronization

As threads become more complex, you will find that they more than likely start to share resources between themselves. The problem with shared resources is that only one thread can safely update them at any one time. Multiple threads that attempt to change a shared resource at the same time will eventually have subtle errors start to occur in themselves.

These errors revolve around the fact that Windows uses preemptive mode multithreading and that C++/CLI commands are not atomic or, in other words, require multiple commands to complete. This combination means that it is possible for a single C++/CLI operation to be interrupted partway

680 C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

through its execution. This, in turn, can lead to a problem if this interruption happens to occur when updating a shared resource.

For example, say two threads are sharing the responsibility of updating a collection of objects based on some shared integer index. As both threads update the collection using the shared index, most of the time everything will be fine, but every once in a while something strange will happen due to the bad timing of the preemptive switch between threads. What happens is that when thread 1 is in the process of incrementing the shared integer index and just as it is about to store the newly incremented index into the shared integer, thread 2 takes control. This thread then proceeds to increment the shared value itself and updates the collection object associated with the index. When thread 1 gets control back, it completes its increment command by storing its increment value in the stored index, overwriting the already incremented value (from thread 2) with the same value. This will cause thread 1 to update the same collection object that thread 2 has already completed. Depending on what updates are being done to the collection, this repeated update could be nasty. For example, maybe the collection was dispersing $1 million to each object in the collection and now that account in question has been dispersed $2 million.

The ThreadStatic Attribute

Sometimes your synchronizing problem is the result of the threads trying to synchronize in the first place. What I mean is you have static class scope variables that store values within a single threaded environment correctly but, when the static variables are migrated to a multithreaded environment, they go haywire.

The problem is that static variables are not only shared by the class, they are also shared between threads. This may be what you want, but there are times when you only want the static variables to be unique between threads.

To solve this, you need to use the System::Threading::ThreadStaticAttribute class. A static variable with an attribute of [ThreadStatic] is not shared between threads. Each thread has its own separate instance of the static variable, which is independently updated. This means that each thread will have a different value in the static variable.

Caution You can’t use the class’s static constructor to initialize a [ThreadStatic] variable because the call to the constructor only initializes the main thread’s instance of the variable. Remember, each thread has its own instance of the [ThreadStatic] variable and that includes the main thread.

Listing 16-7 shows how to create a thread static class variable. It involves nothing more than placing the attribute [ThreadStatic] in front of the variable that you want to make thread static. I added a little wrinkle to this example by making the static variable a handle to an integer. Because the variable is a handle, you need to create an instance of it. Normally, you would do that in the static constructor, but for a thread static variable this doesn’t work, as then only the main thread’s version of the variable has been allocated. To fix this, you need to allocate the static variable within the thread’s execution.

Listing 16-7. Synchronizing Using the ThreadStatic Attribute

using namespace System;

using namespace System::Threading;

ref class MyThread

{

public:

[ThreadStatic]

C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

681

static int ^iVal;

public:

static MyThread()

{

iVal = gcnew int;

}

void ThreadFunc(); void SubThreadFunc();

};

void MyThread::ThreadFunc()

{

iVal = gcnew int; iVal = 7;

SubThreadFunc();

}

void MyThread::SubThreadFunc()

{

int max = *iVal + 5;

while (*iVal < max)

{

Thread ^thr = Thread::CurrentThread; Console::WriteLine("{0} {1}", thr->Name, iVal->ToString()); Thread::Sleep(1);

(*iVal)++;

}

}

void main()

{

Console::WriteLine("Before starting thread");

MyThread ^myThr1 = gcnew MyThread();

Thread ^thr1 =

gcnew Thread(gcnew ThreadStart(myThr1, &MyThread::ThreadFunc)); Thread ^thr2 =

gcnew Thread(gcnew ThreadStart(myThr1, &MyThread::ThreadFunc));

Thread::CurrentThread->Name = "Main"; thr1->Name = "Thread1";

thr2->Name = "Thread2";

thr1->Start(); thr2->Start();

myThr1->iVal = 5; myThr1->SubThreadFunc();

}

682 C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

Unsafe Code Referencing a member variable by address is classified as unsafe, so to get this example to compile, you need to use the /clr:pure or just plain /clr option.

First off, when you comment out the [ThreadStatic] attribute and run the ThreadStaticVars.exe program, you get the output shown in Figure 16-8. Notice how the value is initialized three times and then gets incremented without regard to the thread that is running. Maybe this is what you want, but normally it isn’t.

Figure 16-8. The attribute commented-out ThreadStaticVars program in action

Okay, uncomment the [ThreadStatic] attribute and run ThreadStaticVars.exe again. This time you’ll get the output shown in Figure 16-9. Notice now that each thread (including the main thread) has its own unique instance of the static variable.

Figure 16-9. The ThreadStaticVars program in action

Notice that the static constructor works as expected for the main thread, whereas for worker threads you need to create an instance of the variable before you use it. To avoid having the main thread create a new instance of the static variable, the class separates the logic of initializing the variable from the main logic that the thread is to perform, thus allowing the main thread to call the application’s logic without executing the static variable’s gcnew command.

The Interlocked Class

The opposite of the thread static variable is the interlocked variable. In this case, you want the static variable to be shared across the class and between threads. The Interlocked class provides you with a thread-safe way of sharing an integer type variable (probably used for an index of some sort) between threads.

C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

683

For the sharing of an integer to be thread-safe, the operations to the integer must be atomic. In other words, operations such as incrementing, decrementing, and exchanging variables can’t be preempted partway through the operation. Thus, the $2 million problem from earlier won’t occur.

Using an interlocked variable is fairly straightforward. Instead of using the increment (++) or decrement (--) operator, all you need to do is use the corresponding static System::Threading::Interlocked class method. Notice in the following declarations that you pass a handle to the variable you want interlocked and not the value:

static Int32 Interlocked::Increment(Int32 ^ival); static Int64 Interlocked::Decrement(Int64 ^lval); static Object^ Exchange(&Object^ oval, Object ^oval);

Listing 16-8 shows a thread-safe way of looping using an interlocked variable.

Listing 16-8. Using the Interlocked Class

using namespace System;

using namespace System::Threading;

ref class MyThread

{

static int iVal;

public:

static MyThread()

{

iVal = 5;

}

void ThreadFunc();

};

void MyThread::ThreadFunc()

{

while (Interlocked::Increment(iVal) < 15)

{

Thread ^thr = Thread::CurrentThread; Console::WriteLine("{0} {1}", thr->Name, iVal); Thread::Sleep(1);

}

}

void main()

{

MyThread ^myThr1 = gcnew MyThread();

Thread ^thr1 =

gcnew Thread(gcnew ThreadStart(myThr1, &MyThread::ThreadFunc)); Thread ^thr2 =

gcnew Thread(gcnew ThreadStart(myThr1, &MyThread::ThreadFunc));

684 C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

thr1->Name = "Thread1"; thr2->Name = "Thread2";

thr1->Start(); thr2->Start();

}

Notice that unlike the thread static variable, the static constructor works exactly as it should as there is only one instance of the static variable being shared by all threads.

Figure 16-10 shows InterlockedVars.exe in action, a simple count from 6 to 14, though the count is incremented by different threads.

Figure 16-10. The InterlockedVars program in action

The Monitor Class

The Monitor class is useful if you want a block of code to be executed as single threaded, even if the code block is found in a thread that can be multithreaded. The basic idea is that you use the static methods found in the System::Threading::Monitor class to specify the start and end points of the code to be executed as a single task.

It is possible to have more than one monitor in an application. Therefore, a unique Object is needed for each monitor that you want the application to have. To create the Object to set the Monitor lock on, simply create a standard static Object:

static Object^ MonitorObject = gcnew Object();

You then use this Object along with one of the following two methods to specify the starting point that the Monitor will lock for single thread execution:

Enter() method

TryEnter() method

The Enter() method is the easier and safer of the two methods to use. It has the following syntax:

static void Enter(Object^ MonitorObject);

Basically, the Enter() method allows a thread to continue executing if no other thread is within the code area specified by the Monitor. If another thread occupies the Monitor area, then this thread will sit and wait until the other thread leaves the Monitor area (known as blocking).

The TryEnter() method is a little more complex in that it has three overloads:

static bool TryEnter(Object^ MonitorObject);

static bool TryEnter(Object^ MonitorObject, int wait); static bool TryEnter(Object^ MonitorObject, TimeSpan wait);

The first parameter is the MonitorObject, just like the Enter() method. The second parameter that can be added is the amount of time to wait until you can bypass the block and continue. Yes, you read

C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

685

that right. The TryEnter() method will pass through even if some other thread is currently in the Monitor area. The TryEnter() method will set the start of the Monitor area only if it entered the Monitor when no other thread was in the Monitor area. When the TryEnter() method enters an unoccupied Monitor area, then it returns true; otherwise, it returns false.

This doesn’t sound very safe, does it? If this method isn’t used properly, it isn’t safe. Why would you use this method if it’s so unsafe? It’s designed to allow the programmer the ability to do something other than sit at a blocked monitor and wait, possibly until the application is stopped or the machine reboots. The proper way to use the TryEnter() method is to check the Monitor area. If it’s occupied, wait a specified time for the area to be vacated. If, after that time, it’s still blocked, go do something other than enter the blocked area:

if (!Monitor::TryEnter(MonitorObject))

{

Console::WriteLine("Not able to lock"); return;

}

//...Got lock go ahead

Of course, as you continue into the blocked Monitor area, your code is no longer multithread-safe. Not a thing to do without a very good reason. If you code the TryEnter() method to continue into the Monitor area, even if the area is blocked, be prepared for the program to not work properly.

To set the end of the Monitor area, you use the static Exit() method, which has the following syntax:

static void Exit(Object^ MonitorObject);

Not much to say about this method other than once it’s executed, the Monitor area blocked by either the Entry() method or the TryEnter() method is opened up again for another thread to enter.

In most cases, using these three methods should be all you need. For those rare occasions, the Monitor provides three additional methods that allow another thread to enter a Monitor area even if it’s currently occupied. The first method is the Wait() method, which releases the lock on a Monitor area and blocks the current thread until it reacquires the lock. To reacquire a lock, the block thread must wait for another thread to call a Pulse() or PulseAll() method from within the Monitor area. The main difference between the Pulse() and PulseAll() methods is that Pulse() notifies the next thread waiting that it’s ready to release the Monitor area, whereas PulseAll() notifies all waiting threads.

Listing 16-9 shows how to code threads for a Monitor. The example is composed of three threads. The first two call synchronized Wait() and Pulse() methods, and the last thread calls a TryEnter() method, which it purposely blocks to show how to use the method correctly.

Listing 16-9. Synchronizing Using the Monitor Class

using namespace System;

using namespace System::Threading;

ref class MyThread

{

static Object^ MonitorObject = gcnew Object();

public:

void TFuncOne(); void TFuncTwo(); void TFuncThree();

};

686 C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

void MyThread::TFuncOne()

{

Console::WriteLine("TFuncOne enters monitor"); Monitor::Enter(MonitorObject);

for (Int32 i = 0; i < 3; i++)

{

Console::WriteLine("TFuncOne Waits {0}", i.ToString()); Monitor::Wait(MonitorObject);

Console::WriteLine("TFuncOne Pulses {0}", i.ToString()); Monitor::Pulse(MonitorObject);

Thread::Sleep(1);

}

Monitor::Exit(MonitorObject); Console::WriteLine("TFuncOne exits monitor");

}

void MyThread::TFuncTwo()

{

Console::WriteLine("TFuncTwo enters monitor"); Monitor::Enter(MonitorObject);

for (Int32 i = 0; i < 3; i++)

{

Console::WriteLine("TFuncTwo

Pulses

{0}", i.ToString());

Monitor::Pulse(MonitorObject);

 

 

Thread::Sleep(1);

 

 

 

Console::WriteLine("TFuncTwo

Waits

{0}", i.ToString());

Monitor::Wait(MonitorObject);

 

 

}

 

 

 

Monitor::Exit(MonitorObject);

 

 

 

Console::WriteLine("TFuncTwo

exits monitor");

}

void MyThread::TFuncThree()

{

if (!Monitor::TryEnter(MonitorObject))

{

Console::WriteLine("TFuncThree was not able to lock"); return;

}

Console::WriteLine("TFuncThree got a lock");

Monitor::Exit(MonitorObject); Console::WriteLine("TFuncThree exits monitor");

}

void main()

{

MyThread ^myThr1 = gcnew MyThread();

(gcnew Thread(gcnew ThreadStart(myThr1, &MyThread::TFuncOne)))->Start();

Thread::Sleep(2);

C H A P T E R 1 6 M U L T I T H R E A D E D P R O G R A M M I N G

687

(gcnew Thread(gcnew ThreadStart(myThr1, &MyThread::TFuncTwo)))->Start();

Thread::Sleep(2);

for (int i = 0; i < 2; i++)

{

(gcnew Thread(

gcnew ThreadStart(myThr1, &MyThread::TFuncThree)))->Start(); Thread::Sleep(50);

}

}

Notice that a Monitor area need not be a single block of code but, instead, can be multiple blocks spread out all over the process. In fact, it’s not apparent due to the simplicity of the example, but the Monitor object can be in another class, and the Monitor areas can spread across multiple classes so long as the Monitor object is accessible to all Monitor area classes and the Monitor areas fall within the same process.

The Wait() and Pulse() methods can be tricky to synchronize and, if you fail to call a Pulse() method for a Wait() method, the Wait() method will block until the process is killed or the machine is rebooted. You can add timers to the Wait() method in the same fashion as you do the TryEnter() method, to avoid an infinite wait state. Personally, I think you should avoid using the Wait() and Pulse() methods unless you have no other choice.

Figure 16-11 shows SyncByMonitor.exe in action.

Figure 16-11. The SyncByMonitor program in action

The Mutex Class

The Mutex class is very similar to the Monitor class in the way it synchronizes between threads. You define regions of code that must be single threaded or MUTually EXclusive, and then, when a thread runs, it can only enter the region if no other thread is in the region. What makes the Mutex class special is that it can define regions across processes. In other words, a thread will be blocked in process 1 if some thread in process 2 is in the same name Mutex region.

Before I go into detail about Mutex, let’s sidetrack a little and see how you can have the .NET Framework start one process within another. Creating a process inside another process is fairly easy to do, but within the .NET Framework it’s far from intuitive because the methods to create a process are found within the System::Diagnostic namespace.

The procedure for creating a process is similar to that of a thread in that you create a process and then start it. The actual steps involved in creating a process, though, are a little more involved. To create a process, you simply create an instance using the default constructor: