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 Managed C++ commands are not atomic or, in other words, require multiple commands to complete. This combination means that it is possible for a single Managed C++ operation to be interrupted partway 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 that 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 pointer to an integer. Because the variable is a pointer, 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

start example
 using namespace System; using namespace System::Threading; __gc class MyThread { public:     [ThreadStatic]     static int *iVal; public:     static MyThread()     {         iVal = new int;     }     void ThreadFunc()     {         iVal = new int;         *iVal = 7;         SubThreadFunc();     }     void SubThreadFunc()     {         Int32 max = *iVal + 5;         while (*iVal < max)         {              Thread *thr = Thread::CurrentThread;              Console::WriteLine(S"{0} {1}", thr->Name, iVal->ToString());              Thread::Sleep(1);              (*iVal)++;         }     } }; Int32 main() {     Console::WriteLine(S"Before starting thread");     MyThread *myThr1 = new MyThread();     Thread &thr1 = *new Thread(new ThreadStart(myThr1, &MyThread::ThreadFunc));     Thread &thr2 = *new Thread(new ThreadStart(myThr1, MyThread::ThreadFunc));     Thread::CurrentThread->Name = S"Main";     thr1.Name = S"Thread1";     thr2.Name = S"Thread2";     thr1.Start();     thr2.Start();     (*myThr1->iVal) = 5;     myThr1->SubThreadFunc();     return 0; } 
end example

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.

click to expand
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.

click to expand
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 new 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.

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 pointer 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

start example
 using namespace System; using namespace System::Threading; __gc class MyThread {     static Int32 iVal; public:     static MyThread()     {         iVal = 5;     }     void ThreadFunc()     {         while (Interlocked::Increment(&iVal) < 15)         {              Thread *thr = Thread::CurrentThread;              Console::WriteLine(S"{0} {1}", thr->Name, _box(iVal));              Thread::Sleep(l);         }     } }; Int32 main() {     MyThread *myThr1 = new MyThread();     Thread &thr1 = *new Thread(new ThreadStart(myThr1, &MyThread::ThreadFunc));     Thread &thr2 = *new Thread(new ThreadStart(myThr1, &MyThread::ThreadFunc));     thr1.Name = S"Thread1";     thr2.Name = S"Thread2";     thr1.Start();     thr2.Start();     return 0; } 
end example

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.

click to expand
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 = new 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 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(S"Not able to lock");     return; } //...Got lock go ahead 

Of course, as you continue into the block 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

start example
 using namespace System; using namespace System::Threading; __gc class MyThread {     static Object* MonitorObject = new Object(); public:     void ThreadFuncOne()     {         Console::WriteLine(S"Func1 enters monitor");         Monitor::Enter(MonitorObject);         for (Int32 i = 0; i < 3; i++)         {             Console::WriteLine(S"Func1 Waits {0}", i.ToString());             Monitor::Wait(MonitorObject);             Console::WriteLine(S"Func1 Pulses {0}", i.ToString());             Monitor::Pulse(MonitorObject);             Thread::Sleep(1);         }         Monitor::Exit(MonitorObject);         Console::WriteLine(S"Func1 exits monitor"); }     void ThreadFunctwo()     {         Console::WriteLine(S"Func2 enters monitor");         Monitor::Enter(MonitorObject);         for (Int32 i = 0; i < 3; i++)         {             Console::WriteLine(S"Func2 Pulses {0}", i.ToString());             Monitor::Pulse(MonitorObject);             Thread::Sleep(1);             Console::WriteLine(S"Func2 Waits {0}", i.ToString());             Monitor::Wait(MonitorObject);         }         Monitor::Exit(MonitorObject);         Console::WriteLine(S"Func2 exits monitor");     }     void ThreadFuncthree()     {         if (!Monitor::TryEnter(MonitorObject))         {             Console::WriteLine(S"Func3 was not able to lock");             return;         }         Console::WriteLine(S"Func3 got a lock");         Monitor::Exit(MonitorObject);         Console::WriteLine(S"Func3 exits monitor");     } }; Int32 main() {     MyThread *myThr1 = new MyThread();     (new Thread(new ThreadStart(myThr1, &MyThread::ThreadFuncOne)))->Start();     (new Thread(new ThreadStart(myThr1, &MyThread::ThreadFunctwo)))->Start();     (new Thread(new ThreadStart(myThr1, &MyThread::ThreadFuncthree)))->Start();     return 0; } 
end example

Notice that a Monitor area need not be a single block of code but, instead, it 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.

click to expand
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 process 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:

 Process* proc = new Process(); 

Next, you need to populate several properties found in the StartInfo property. These properties will tell the Framework where the process is, what parameters to pass, whether to start the process in its own shell, and whether to redirect standard input. There are several other properties as well but these are the most important:

 proc->StartInfo->FileName = S"../debug/SyncByMutex.exe"; proc->StartInfo->Arguments = S"1"; proc->StartInfo->UseShellExecute = false; proc->StartInfo->RedirectStandardInput = true; 

Finally, once the process is defined, you start it:

 proc->Start(); 

Listing 16-10 shows how to start two copies of the Mutex process that you will build next in this chapter.

Listing 16-10: Creating Subprocesses

start example
 using namespace System; using namespace System::Diagnostics; using namespace System::Threading; Int32 main() {     Process* prod = new Process();     proc1->StartInfo->FileName = S"../debug/SyncByMutex.exe";     proc1->StartInfo->Arguments = S"1";     proc1->StartInfo->UseShellExecute = false;     proc1->StartInfo->RedirectStandardInput = true;     proc1->Start();     Thread::Sleep(20);     Process* proc2 = new Process();     proc2->StartInfo->FileName = S"../debug/SyncByMutex.exe";     proc2->StartInfo->Arguments = S"2";     proc2->StartInfo->UseShellExecute = false;     proc2->StartInfo->RedirectStandardInput = true;     proc2->Start();     Thread::Sleep(2000); // added just to clean up console display     return 0; } 
end example

You don't need to use MutexSpawn.exe to run the following Mutex example, but it makes things easier when you're trying to test multiple processes running at the same time.

Okay, let's move on to actually looking at the Mutex class. In general, you'll use only three methods on a regular basis within the Mutex class:

  • The constructor

  • WaitOne()

  • ReleaseMutex()

Unlike the Monitor class, in which you use a static member, the Mutex class requires you to create an instance and then access its member methods. Like any other class, to create an instance of Mutex requires you call its constructor. The Mutex constructor provides four overloads:

 Mutex(); Mutex(Boolean owner); Mutex(Boolean owner, String* name); Mutex(Boolean owner, String* name, Boolean * createdNew); 

When you create the Mutex object, you specify whether you want it to have ownership of the Mutex or, in other words, block the other threads trying to enter the region. Be careful, though, that the constructor doesn't cause a thread to block. This requires the use of the WaitOne() method, which you'll see later in the chapter.

You can create either a named or unnamed instance of a Mutex object but, to share a Mutex across processes, you need to give it a name. When you provide a Mutex with a name, the Mutex constructor will look for another Mutex with the same name. If it does find one, then they will synchronize blocks of code together.

The last constructor adds an output parameter that will have a value of true if this call was the first constructor to build a Mutex of the specified name; otherwise, the name already exists and will have the value of false.

Once a Mutex object exists, you then must tell it to wait for the region to be unoccupied before entering. You do this using the Mutex class's WaitOne() member method:

 bool WaitOne(); bool WaitOne(int milliseconds, bool /*not used in Mutex*/); bool WaitOne(TimeSpan span, bool /*not used in Mutex*/); 

The WaitOne() method is similar to a combination of the Monitor class's Enter() and TryEnter() methods, in that the WaitOne() method will wait indefinitely like the Monitor::Enter() method if you pass it no parameters. If you pass it parameters, though, it blocks for the specified time and then passes through like the Monitor::TryEnter() method. Just like the TryEnter() method, you should not, normally, let the thread execute the code within the Mutex region, as that will make the region not thread-safe.

To specify the end of the Mutex region, you use the Mutex class's ReleaseMutex() member method. Just like Monitor's Enter() and Exit() method combination, you need to match WaitOne() calls with ReleaseMutex() calls.

Listing 16-11 shows how to code a multithreaded single process. There is nothing special about it. In fact, I would normally just use a Monitor. Where this example really shines is when it is used in conjunction with MutexSpawn.exe, as it shows the Mutex class's real power of handling mutually exclusive regions of code across processes.

Listing 16-11: Synchronizing Using the Mutex Class

start example
 using namespace System; using namespace System::Threading; __gc class MyThread { public:     void ThreadFunc()     {         Thread *thr = Thread::CurrentThread;         Mutex *m;         for (Int32 i = 0; i < 4; i++)         {             m = new Mutex(true, "SyncByMutex");             m->WaitOne();             Console::WriteLine(S"{0} - {1}", thr->Name, __box(i));             m->ReleaseMutex();             Thread::Sleep(100);         }     } }; Int32 main(Int32 argc, SByte __nogc *argv[]) {     MyThread *myThr = new MyThread();     Thread &thr1 = *new Thread(new ThreadStart(myThr, &MyThread::ThreadFunc));     Thread &thr2 = *new Thread(new ThreadStart(myThr, &MyThread::ThreadFunc));     thr1.Name = String::Format(S"Process {0} - Thread 1", new String(argv[1]));     thr2.Name = String::Format(S"Process {0} - Thread 2", new String(argv[1]));     thr1.Start();     Thread::Sleep(500);     thr2.Start();     return 0; } 
end example

Because you've already seen how to use the Monitor, the preceding example should be quite straightforward. The only real difference (other than the names of the methods being different, of course) is that the Mutex uses an instance object and member method calls, and the Monitor uses static method calls.

Figure 16-12 shows SyncByMutex.exe in action. Notice that threads in both processes are blocked and get access to the named Mutex region.

click to expand
Figure 16-12: A pair of SyncByMutex programs in action

The ReaderWriterLock Class

The System::Threading::ReaderWriterLock class is a little different from the previous two types of synchronization in that it uses a multiple-reader/single-writer mechanism instead of the all-or-nothing approach. What this means is that the ReaderWriterLock class allows any number of threads to be in a block of synchronized code so long as they are only reading the shared resource within it. On the other hand, if a thread needs to change the shared resource, then all threads must vacate the region and give the updating thread exclusive access to it.

This type of synchronization makes sense because if a thread isn't changing anything, then it can't affect other threads. So, why not give the thread access to the shared resource?

The ReaderWriterLock class is very similar to both the Monitor class and the Mutex class. You specify a region to be synchronized and then have the threads block or pass into this area based on whether an update is happening in the region.

Like the Mutex class, you create an instance of the ReaderWriterLock class and work with its member method. To create an instance of the ReaderWriterLock object, you call its default constructor:

 ReaderWriterLock(); 

Once you have a ReaderWriterLock object, you need to determine if the region of code you want to block will do only reading of the shared resource or if it will change the shared resource.

If the region will only read the shared resource, then use the following code to set the region as read-only:

 void AcquireReaderLock(int milliseconds); void AcquireReaderLock(TimeSpan span); 

You pass both of these overloaded methods a parameter, so specify the length of time you're willing to wait before entering the region. Due to the nature of this synchronization method, you can be sure of one thing: If you're blocked by this method call, then some other thread is currently updating the shared resource within. The reason you know some other thread is writing to the region is because the thread doesn't block if other threads in the region are only reading the shared resource.

Because you know that some thread is writing in the region, you should make the time you wait longer than the time needed to complete the write process. Unlike any of the other synchronization methods you've seen in this chapter, when this method times out, it throws an ApplicationException exception. So if you specify anything other than an infinite wait, you should catch the exception. The reason these methods throw an exception is that the only reason the wait time should expire is due to a thread deadlock condition. Deadlock is when two threads wait forever for each other to complete.

To specify the end of a synchronized read-only region, you need to release the region:

 void ReleaseReaderLock(); 

If the region will require updating of the shared resource within the region, then you need to acquire a different lock:

 void AcquireWriterLock(int milliseconds); void AcquireWriterLock(TimeSpan span); 

Like the reader, these methods pass parameters to avoid the deadlock situation. Unlike the reader lock, though, these methods block no matter what type of thread falls within the region, because they allow only one thread to have access. If you were to use only writer locks, then you would, in effect, be coding a Monitor or a Mutex.

As you would expect, once you're finished with the writer region, you need to release it:

 void ReleaseWriterLock(); 

Listing 16-12 shows how to implement a multithread application using ReaderWriterLock. Also, just for grins and giggles, I added an Interlocked::Decrement() method to show you how that works as well.

Listing 16-12: Synchronizing Using the ReaderWriterLock Class

start example
 using namespace System; using namespace System::Threading; __gc class MyThread {     static ReaderWriterLock *RWLock = new ReaderWriterLock();     static Int32 iVal = 4; public:     static void ReaderThread()     {         String *thrName = Thread::CurrentThread->Name;         while (true)         {             try             {                 RWLock->AcquireReaderLock(-1); // Wait forever                 Console::WriteLine(S"Reading in {0}. iVal is {1}",                     thrName, __box(iVal));                 RWLock->ReleaseReaderLock();                 Thread::Sleep(4);             }             catch (ApplicationException*)             {                 Console::WriteLine(S"Reading in {0}. Timed out", thrName);             }         }     }     static void WriterThread()     {         while (iVal > 0)         {             RWLock->AcquireWriterLock(-1); // wait forever             Interlocked::Decrement(&iVal);             Console::WriteLine(S"Writing iVal to {0}", __box(iVal));             Thread::Sleep(20);             RWLock->ReleaseWriterLock();         }     } }; Int32 main() { {     Thread &thr1 = *new Thread(new ThreadStart(0, &MyThread::ReaderThread));     Thread &thr2 = *new Thread(new ThreadStart(0, &MyThread::ReaderThread));     Thread &thr3 = *new Thread(new ThreadStart(0, &MyThread::WriterThread));     thr1.Name = S"Thread1";     thr2.Name = S"Thread2";     thr1.IsBackground = true;     thr2.IsBackground = true;     thr1.Start();     thr2.Start();     thr3.Start();     thr3.Join();     Thread::Sleep(2);     return 0; } 
end example

In actuality, the preceding code shouldn't need to use Interlock because the region is already locked for synchronization. Notice that I created infinite loops for my reader threads. To get these threads to exit at the completion of the program, I made the background threads.

Figure 16-13 shows SyncByRWLock.exe in action. Notice that I purposely don't specify a long-enough wait for the writing process to complete so that the exception is thrown.

click to expand
Figure 16-13: The SyncByRWLock program in action




Managed C++ and. NET Development
Managed C++ and .NET Development: Visual Studio .NET 2003 Edition
ISBN: 1590590333
EAN: 2147483647
Year: 2005
Pages: 169

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