Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
As you might imagine, it s much messier to break out of the
middle of a Runnable::run( ) function than it is to wait for that
function to get to a test of isCanceled( ) (or some other place
where the programmer is ready to leave the function). When you break out of a
blocked task, you might need to destroy objects and clean up resources. Because
of this, breaking out of the middle of a task s run( ) is more like
throwing an exception than anything else, so in ZThreads, exceptions are used
for this kind of abort. (This walks the fine edge of being an inappropriate use
of exceptions, because it means you are often using them for control flow.) To return to
a known good state when terminating a task this way, carefully consider the
execution paths of your code and properly clean up everything inside the catch
clause. We ll look at these issues in this section.
To terminate a blocked thread, the ZThread library provides
the Thread::interrupt( ) function. This sets the interrupted
status for that thread. A thread with its interrupted status set will throw
an Interrupted_Exception if it is already blocked or it attempts a
blocking operation. The interrupted status will be reset when the exception is
thrown or if the task calls Thread::interrupted( ). As you ll see, Thread::interrupted( )
provides a second way to leave your run( ) loop, without throwing
an exception.
Here s an example that shows the basics of interrupt( ):
//: C11:Interrupting.cpp
// Interrupting a blocked thread.
//{L} ZThread
#include <iostream>
#include "zthread/Thread.h"
using namespace ZThread;
using namespace std;
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, in addition to the insertion into cout,
run( ) contains two other 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 call cin.get( ). If you don t
give the program an argument, the sleep( ) in main( )
is skipped. Here, 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
If you try to call a function whose mutex has already been
acquired, 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 <iostream>
#include "zthread/Thread.h"
#include "zthread/Mutex.h"
#include "zthread/Guard.h"
using namespace ZThread;
using namespace std;
class BlockedMutex {
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 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 handle both blocked and
non-blocked possibilities when the interrupted status is set:
//: C11:Interrupting3.cpp {RunByHand}
// General idiom for interrupting a task.
//{L} ZThread
#include <iostream>
#include "zthread/Thread.h"
using namespace ZThread;
using namespace std;
const double PI = 3.14159265358979323846;
const double E = 2.7182818284590452354;
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 + (PI + 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;
}
} ///:~
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.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |