I l @ ve RuBoard |
Even if you're already familiar with thread-safety issues and know why and how to share resources between threads safely by serializing access using mutexes , skim this section anyway. It forms the basis for the more-detailed examples later on. In a nutshell , things are a bit different if our program is multithreaded. Functions like GetError() and SetError() might well be called at the same time on different threads. In that case, calls to these functions could be interlaced, and they will no longer work properly as originally written above. For example, consider what might happen if the following two pieces of code could be executed at the same time: // thread A String newerr = SetError( "A" ); newerr += " (set by A)"; cout << newerr << endl; // thread B String newerr = SetError( "B" ); newerr += " (set by B)"; cout << newerr << endl; There are many ways in which this can go wrong. Here's one: Say thread A is running and, inside the SetError( "A" ) call, gets as far as setting err to " 1 :" before the operating system decides to preempt it and switch to thread B. Thread B executes completely; then thread A is reactivated and runs to completion. The output would be 2: B (12:01:09.125) (set by B) 2: B (12:01:09.125)A (12:01:09.195) (set by A) It's easy to invent situations that produce even stranger output, [1] but in reality you'd be lucky to get anything this sensible . Because a thread's execution can be preempted anywhere , including right in the middle of a String operation, such as String::operator+=() itself, you're far more likely to see just intermittent crashes, as String member functions on different threads attempt to update the same String object at the same time. (If you don't see this problem right away, try writing those String member functions and see what happens if their execution gets interleaved.)
The way to correct this is to make sure that only one thread can be working with the shared resources at a time. We prevent the functions from getting interlaced by "serializing" them with a mutex or similar device. But who should be responsible for doing the serialization? There are two levels at which we could do it. The main trade-off is that the lower the level at which the work is done, the more locking needs to be done, often needlessly. That's because the lower levels don't know whether acquiring a lock is necessary for a given operation, so they have to do it every time, just in case. Excessive locking is a major concern because acquiring a mutex lock is typically an expensive operation on most systems, approaching or surpassing the cost of a general-purpose dynamic memory allocation.
In our example, implementing Option 2 means that the Error subsystem should take responsibility for serializing access to the String object that it owns. Here's a typical way to do it: // Example A-2: A thread-safe error recording subsystem // String err; int count = 0; Mutex m;// to protect the err and count values String GetError() { Lock<Mutex> l(m); //--enter mutual exclusion block------ String ret = err; l.Unlock(); //--exit mutual exclusion block------- return ret; } string SetError( const String& msg ) { Lock<Mutex> l(m); //--enter mutual exclusion block------ err = AsString( ++count ) + ": "; err += msg; err += " (" + TimeAsString() + ")"; String ret = err; l.Unlock(); //--exit mutual exclusion block------- return ret; } All is well, because each function body is now atomic as far as err and count are concerned and no interlacing will occur. SetError() calls are automatically serialized so that their bodies do not overlap at all. For more details about threads, critical sections, mutexes, semaphores, race conditions, the Dining Philosophers Problem, and lots of interesting related topics, see any good text on operating systems or multithreaded programming. From here on, I'll assume you know the basics.
|
I l @ ve RuBoard |