1. Trang chủ
  2. » Công Nghệ Thông Tin

thinking in c 2nd ed volume 2 rev 20 - phần 10 pptx

48 258 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 48
Dung lượng 125,1 KB

Nội dung

469 z 516 class Blocked : public Runnable { public: void run() { try { Thread::sleep(1000); cout << "Waiting for get() in run():"; cin.get(); } catch(Interrupted_Exception&) { cout << "Caught Interrupted_Exception" << endl; // Exit the task } } }; int main(int argc, char* argv[]) { try { Thread t(new Blocked); if(argc > 1) Thread::sleep(1100); t.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } } ///:~ You can see that run( ) contains two points where blocking can occur: the call to Thread::sleep (1000) and the call to cin.get( ). By giving the program any command-line argument, you tell main( ) to sleep long enough that the task will finish its sleep( ) and move into the cin.get( ). If you don’t give the program an argument, the sleep( ) in main( ) is skipped. In this case, the call to interrupt( ) will occur while the task is sleeping, and you’ll see that this will cause Interrupted_Exception to be thrown. If you give the program a command-line argument, you’ll discover that a task cannot be interrupted if it is blocked on IO. That is, you can interrupt out of any blocking operation except IO. This is a little disconcerting if you’re creating a thread that performs IO, because it means that I/O has the potential of locking your multithreaded program. The problem is that, again, C++ was not designed with threading in mind; quite the opposite, it effectively pretends that threading doesn’t exist. Thus, the iostream library is not thread-friendly. If the new C++ standard decides to add thread support, the iostream library may need to be reconsidered in the process. Blocked by a mutex In the previous list of ways to become blocked, the last one happens when you’re trying to call a function whose mutex has already been acquired. In this situation, the calling task will be suspended until the mutex becomes available. The following example tests whether this kind of blocking is interruptible: //: C11:Interrupting2.cpp // Interrupting a thread blocked // with a synchronization guard. //{L} ZThread #include "zthread/Thread.h" #include "zthread/Mutex.h" #include "zthread/Guard.h" #include <iostream> using namespace ZThread; using namespace std; class BlockedMutex { 470 z 516 Mutex lock; public: BlockedMutex() { lock.acquire(); } void f() { Guard<Mutex> g(lock); // This will never be available } }; class Blocked2 : public Runnable { BlockedMutex blocked; public: void run() { try { cout << "Waiting for f() in BlockedMutex" << endl; blocked.f(); } catch(Interrupted_Exception& e) { cerr << e.what() << endl; // Exit the task } } }; int main(int argc, char* argv[]) { try { Thread t(new Blocked2); t.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } } ///:~ The class BlockedMutex has a constructor that acquires the object’s own Mutex and never releases it. For that reason, if you try to call f( ), you will always be blocked because the Mutex cannot be acquired. In Blocked2, the run( ) function will therefore be stopped at the call to blocked.f( ). When you run the program you’ll see that, unlike the iostream call, interrupt( ) can break out of a call that’s blocked by a mutex. Checking for an interrupt Note that when you call interrupt( ) on a thread, the only time that the interrupt occurs is when the task enters, or is already inside, a blocking operation (except, as you’ve seen, in the case of IO, where you’re just stuck). But what if you’ve written code that may or may not make such a blocking call, depending on the conditions in which it is run? If you can only exit by throwing an exception on a blocking call, you won’t always be able to leave the run( ) loop. Thus, if you call interrupt( ) to stop a task, your task needs a second opportunity to exit in the event that your run( ) loop doesn’t happen to be making any blocking calls. This opportunity is presented by the interrupted status, which is set by the call to interrupt( ). You check for the interrupted status by calling interrupted( ). This not only tells you whether interrupt( ) has been called, it also clears the interrupted status. Clearing the interrupted status ensures that the framework will not notify you twice about a task being interrupted. You will be notified via either a single Interrupted_Exception, or a single successful Thread::interrupted( ) test. If you want to check again to see whether you were interrupted, you can store the result when you call Thread::interrupted( ). The following example shows the typical idiom that you should use in your run( ) function to 471 z 516 handle both blocked and non-blocked possibilities when the interrupted status is set: //: C11:Interrupting3.cpp // General idiom for interrupting a task. //{L} ZThread #include "zthread/Thread.h" #include <iostream> #include <cmath> #include <cstdlib> using namespace ZThread; using namespace std; class NeedsCleanup { int id; public: NeedsCleanup(int ident) : id(ident) { cout << "NeedsCleanup " << id << endl; } ~NeedsCleanup() { cout << "~NeedsCleanup " << id << endl; } }; class Blocked3 : public Runnable { volatile double d; public: Blocked3() : d(0.0) {} void run() { try { while(!Thread::interrupted()) { point1: NeedsCleanup n1(1); cout << "Sleeping" << endl; Thread::sleep(1000); point2: NeedsCleanup n2(2); cout << "Calculating" << endl; // A time-consuming, non-blocking operation: for(int i = 1; i < 100000; i++) d = d + (M_PI + M_E) / (double)i; } cout << "Exiting via while() test" << endl; } catch(Interrupted_Exception&) { cout << "Exiting via Interrupted_Exception" << endl; } } }; int main(int argc, char* argv[]) { if(argc != 2) { cerr << "usage: " << argv[0] << " delay-in-milliseconds" << endl; exit(1); } int delay = atoi(argv[1]); try { Thread t(new Blocked3); Thread::sleep(delay); t.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; 472 z 516 } } ///:~ The NeedsCleanup class emphasizes the necessity of proper resource cleanup if you leave the loop via an exception. Note that no pointers are defined in Blocked3::run( ) because, for exception safety, all resources must be enclosed in stack-based objects so that the exception handler can automatically clean them up by calling the destructor. You must give the program a command-line argument which is the delay time in milliseconds before it calls interrupt( ). By using different delays, you can exit Blocked3::run( ) at different points in the loop: in the blocking sleep( ) call, and in the non-blocking mathematical calculation. You’ll see that if interrupt( ) is called after the label point2 (during the non- blocking operation), first the loop is completed, then all the local objects are destructed, and finally the loop is exited at the top via the while statement. However, if interrupt( ) is called between point1 and point2 (after the while statement but before or during the blocking operation sleep( )), the task exits via the Interrupted_Exception. In that case, only the stack objects that have been created up to the point where the exception is thrown are cleaned up, and you have the opportunity to perform any other cleanup in the catch clause. A class designed to respond to an interrupt( ) must establish a policy that ensures it will remain in a consistent state. This generally means that all resource acquisition should be wrapped inside stack-based objects so that the destructors will be called regardless of how the run( ) loop exits. Correctly done, code like this can be elegant. Components can be created that completely encapsulate their synchronization mechanisms but are still responsive to an external stimulus (via interrupt( )) without adding any special functions to an object’s interface. Cooperation between threads As you’ve seen, when you use threads to run more than one task at a time, you can keep one task from interfering with another task’s resources by using a mutex to synchronize the behavior of the two tasks. That is, if two tasks are stepping on each other over a shared resource (usually memory), you use a mutex to allow only one task at a time to access that resource. With that problem solved, you can move on to the issue of getting threads to cooperate, so that multiple threads can work together to solve a problem. Now the issue is not about interfering with one another, but rather about working in unison, since portions of such problems must be solved before other portions can be solved. It’s much like project planning: the footings for the house must be dug first, but the steel can be laid and the concrete forms can be built in parallel, and both of those tasks must be finished before the concrete foundation can be poured. The plumbing must be in place before the concrete slab can be poured, the concrete slab must be in place before you start framing, and so on. Some of these tasks can be done in parallel, but certain steps require all tasks to be completed before you can move ahead. The key issue when tasks are cooperating is handshaking between those tasks. To accomplish this handshaking, we use the same foundation: the mutex, which in this case guarantees that only one task can respond to a signal. This eliminates any possible race conditions. On top of the mutex, we add a way for a task to suspend itself until some external state changes (“the plumbing is now in place”), indicating that it’s time for that task to move forward. In this section, we’ll look at the issues of handshaking between tasks, the problems that can arise, and their solutions. Wait and signal In ZThreads, the basic class that uses a mutex and allows task suspension is the Condition, and you can suspend a task by calling wait( ) on a Condition. When external state changes take place that might mean that a task should continue processing, you notify the task by calling signal( ), to wake up one task, or broadcast( ), to wake up all tasks that have suspended 473 z 516 themselves on that Condition object. There are two forms of wait( ). The first takes an argument in milliseconds that has the same meaning as in sleep( ): “pause for this period of time.” The difference is that in a timed wait( ): 1. The Mutex that is controlled by the Condition object is released during the wait( ). 2. You can come out of the wait( ) due to a signal( ) or a broadcast( ), as well as by letting the clock run out. The second form of wait( ) takes no arguments; this version is more commonly used. It also releases the mutex, but this wait( ) suspends the thread indefinitely until that Condition object receives a signal( ) or broadcast( ). Typically, you use wait( ) when you’re waiting for some condition to change that is under the control of forces outside the current function. (Often, this condition will be changed by another thread.) You don’t want to idly loop while testing the condition inside your thread; this is called a “busy wait,” and it’s a bad use of CPU cycles. Thus, wait( ) allows you to suspend the thread while waiting for the world to change, and only when a signal( ) or broadcast( ) occurs (suggesting that something of interest may have happened), does the thread wake up and check for changes. Therefore, wait( ) provides a way to synchronize activities between threads. Let’s look at a simple example. WaxOMatic.cpp applies wax to a Car and polishes it using two separate processes. The polishing process cannot do its job until the application process is finished, and the application process must wait until the polishing process is finished before it can put on another coat of wax. Both WaxOn and WaxOff use the Car object, which contains a Condition that it uses to suspend a thread inside waitForWaxing( ) or waitForBuffing( ): //: C11:WaxOMatic.cpp // Basic thread cooperation. //{L} ZThread #include "zthread/Thread.h" #include "zthread/Mutex.h" #include "zthread/Guard.h" #include "zthread/Condition.h" #include "zthread/ThreadedExecutor.h" #include <iostream> #include <string> using namespace ZThread; using namespace std; class Car { Mutex lock; Condition condition; bool waxOn; public: Car() : condition(lock), waxOn(false) {} void waxed() { Guard<Mutex> g(lock); waxOn = true; // Ready to buff condition.signal(); } void buffed() { Guard<Mutex> g(lock); waxOn = false; // Ready for another coat of wax condition.signal(); } void waitForWaxing() { Guard<Mutex> g(lock); 474 z 516 while(waxOn == false) condition.wait(); } void waitForBuffing() { Guard<Mutex> g(lock); while(waxOn == true) condition.wait(); } }; class WaxOn : public Runnable { CountedPtr<Car> car; public: WaxOn(CountedPtr<Car>& c) : car(c) {} void run() { try { while(!Thread::interrupted()) { cout << "Wax On!" << endl; Thread::sleep(200); car->waxed(); car->waitForBuffing(); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Ending Wax On process" << endl; } }; class WaxOff : public Runnable { CountedPtr<Car> car; public: WaxOff(CountedPtr<Car>& c) : car(c) {} void run() { try { while(!Thread::interrupted()) { car->waitForWaxing(); cout << "Wax Off!" << endl; Thread::sleep(200); car->buffed(); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Ending Wax Off process" << endl; } }; int main() { cout << "Press <Enter> to quit" << endl; try { CountedPtr<Car> car(new Car); ThreadedExecutor executor; executor.execute(new WaxOff(car)); executor.execute(new WaxOn(car)); cin.get(); executor.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } } ///:~ In Car’s constructor, a single Mutex is wrapped in a Condition object so that it can be used to manage inter-task communication. However, the Condition object contains no information about the state of your process, so you need to manage additional information to indicate process 475 z 516 state. Here, Car has a single bool waxOn, which indicates the state of the waxing-polishing process. In waitForWaxing( ), the waxOn flag is checked, and if it is false, the calling thread is suspended by calling wait( ) on the Condition object. It’s important that this occur inside a guarded clause, in which the thread has acquired the lock (here, by creating a Guard object). When you call wait( ), the thread is suspended and the lock is released. It is essential that the lock be released because, to safely change the state of the object (for example, to change waxOn to true, which must happen if the suspended thread is to ever continue), that lock must be available to be acquired by some other task. In this example, when another thread calls waxed( ) to tell it that it’s time to do something, the mutex must be acquired in order to change waxOn to true. Afterward, waxed( ) sends a signal( ) to the Condition object, which wakes up the thread suspended in the call to wait( ). Although signal( ) may be called inside a guarded clause—as it is here—you are not required to do this. In order for a thread to wake up from a wait( ), it must first reacquire the mutex that it released when it entered the wait( ). The thread will not wake up until that mutex becomes available. The call to wait( ) is placed inside a while loop that checks the condition of interest. This is important for two reasons: • It is possible that when the thread gets a signal( ), some other condition has changed that is not associated with the reason that we called wait( ) here. If that is the case, this thread should be suspended again until its condition of interest changes. • By the time this thread awakens from its wait( ), it’s possible that some other task has changed things such that this thread is unable or uninterested in performing its operation at this time. Again, it should be re-suspended by calling wait( ) again. Because these two reasons are always present when you are calling wait( ), always write your call to wait( ) inside a while loop that tests for your condition(s) of interest. WaxOn::run( ) represents the first step in the process of waxing the car, so it performs its operation (a call to sleep( ) to simulate the time necessary for waxing). It then tells the car that waxing is complete, and calls waitForBuffing( ), which suspends this thread with a wait( ) until the WaxOff process calls buffed( ) for the car, changing the state and calling notify( ). WaxOff::run( ), on the other hand, immediately moves into waitForWaxing( ) and is thus suspended until the wax has been applied by WaxOn and waxed( ) is called. When you run this program, you can watch this two-step process repeat itself as control is handed back and forth between the two threads. When you press the <Enter> key, interrupt( ) halts both threads— when you call interrupt( ) for an Executor, it calls interrupt( ) for all the threads it is controlling. Producer-consumer relationships A common situation in threading problems is the producer-consumer relationship, in which one task is creating objects and other tasks are consuming them. In such a situation, make sure that (among other things) the consuming tasks do not accidentally skip any of the produced objects. To show this problem, consider a machine that has three tasks: one to make toast, one to butter the toast, and one to put jam on the buttered toast. //: C11:ToastOMatic.cpp // Problems with thread cooperation. //{L} ZThread #include "zthread/Thread.h" [127] 476 z 516 #include "zthread/Mutex.h" #include "zthread/Guard.h" #include "zthread/Condition.h" #include "zthread/ThreadedExecutor.h" #include <iostream> #include <cstdlib> #include <ctime> using namespace ZThread; using namespace std; // Apply jam to buttered toast: class Jammer : public Runnable { Mutex lock; Condition butteredToastReady; bool gotButteredToast; int jammed; public: Jammer(): butteredToastReady(lock) { gotButteredToast = false; jammed = 0; } void moreButteredToastReady() { Guard<Mutex> g(lock); gotButteredToast = true; butteredToastReady.signal(); } void run() { try { while(!Thread::interrupted()) { { Guard<Mutex> g(lock); while(!gotButteredToast) butteredToastReady.wait(); jammed++; } cout << "Putting jam on toast " << jammed << endl; { Guard<Mutex> g(lock); gotButteredToast = false; } } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Jammer off" << endl; } }; // Apply butter to toast: class Butterer : public Runnable { Mutex lock; Condition toastReady; CountedPtr<Jammer> jammer; bool gotToast; int buttered; public: Butterer(CountedPtr<Jammer>& j) : jammer(j), toastReady(lock) { gotToast = false; buttered = 0; } void moreToastReady() { Guard<Mutex> g(lock); 477 z 516 gotToast = true; toastReady.signal(); } void run() { try { while(!Thread::interrupted()) { { Guard<Mutex> g(lock); while(!gotToast) toastReady.wait(); buttered++; } cout << "Buttering toast " << buttered << endl; jammer->moreButteredToastReady(); { Guard<Mutex> g(lock); gotToast = false; } } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Butterer off" << endl; } }; class Toaster : public Runnable { CountedPtr<Butterer> butterer; int toasted; public: Toaster(CountedPtr<Butterer>& b) : butterer(b) { toasted = 0; srand(time(0)); // Seed the random number generator } void run() { try { while(!Thread::interrupted()) { Thread::sleep(rand()/(RAND_MAX/5)*100); // // Create new toast // cout << "New toast " << ++toasted << endl; butterer->moreToastReady(); } } catch(Interrupted_Exception&) { /* Exit */ } cout << "Toaster off" << endl; } }; int main() { try { cout << "Press <Return> to quit" << endl; CountedPtr<Jammer> jammer(new Jammer); CountedPtr<Butterer> butterer(new Butterer(jammer)); ThreadedExecutor executor; executor.execute(new Toaster(butterer)); executor.execute(butterer); executor.execute(jammer); cin.get(); executor.interrupt(); } catch(Synchronization_Exception& e) { cerr << e.what() << endl; } 478 z 516 } ///:~ The classes are defined in the reverse order that they operate to simplify forward-referencing issues. Jammer and Butterer both contain a Mutex, a Condition, and some kind of internal state information that changes to indicate that the process should suspend or resume. (Toaster doesn’t need these since it is the producer and doesn’t have to wait on anything.) The two run( ) functions perform an operation, set a state flag, and then call wait( ) to suspend the task. The moreToastReady( ) and moreButteredToastReady( ) functions change their respective state flags to indicate that something has changed and the process should consider resuming and then call signal( ) to wake up the thread. The difference between this example and the previous one is that, at least conceptually, something is being produced here: toast. The rate of toast production is randomized a bit, to add some uncertainty. And you’ll see that when you run the program, things aren’t going right, because many pieces of toast appear to be getting dropped on the floor—not buttered, not jammed. Solving threading problems with queues Often, threading problems are based on the need for tasks to be serialized—that is, to take care of things in order. ToastOMatic.cpp must not only take care of things in order, it must be able to work on one piece of toast without worrying that toast is falling on the floor in the meantime. You can solve many threading problems by using a queue that synchronizes access to the elements within: //: C11:TQueue.h #ifndef TQUEUE_H #define TQUEUE_H #include "zthread/Thread.h" #include "zthread/Condition.h" #include "zthread/Mutex.h" #include "zthread/Guard.h" #include <deque> template<class T> class TQueue { ZThread::Mutex lock; ZThread::Condition cond; std::deque<T> data; public: TQueue() : cond(lock) {} void put(T item) { ZThread::Guard<ZThread::Mutex> g(lock); data.push_back(item); cond.signal(); } T get() { ZThread::Guard<ZThread::Mutex> g(lock); while(data.empty()) cond.wait(); T returnVal = data.front(); data.pop_front(); return returnVal; } }; #endif // TQUEUE_H ///:~ This builds on the Standard C++ Library deque by adding: [...]... readyCondition(readyLock) { occupied = false; engineBotHired = true; wheelBotHired = true; driveTrainBotHired = true; } void insertCar(Car chassis) { c = chassis; occupied = true; } Car getCar() { // Can only extract car once if(!occupied) { cerr . points in the loop: in the blocking sleep( ) call, and in the non-blocking mathematical calculation. You’ll see that if interrupt( ) is called after the label point2 (during the non- blocking. try { CountedPtr<Car> car(new Car); ThreadedExecutor executor; executor.execute(new WaxOff(car)); executor.execute(new WaxOn(car)); cin.get(); executor.interrupt(); } catch(Synchronization_Exception&. that no pointers are defined in Blocked3::run( ) because, for exception safety, all resources must be enclosed in stack-based objects so that the exception handler can automatically clean them

Ngày đăng: 13/08/2014, 09:20