As described in Section 28.9, the server-side Ice run time by default creates a thread pool for you and automatically dispatches each incoming request in its own thread. As a result, you usually only need to worry about synchronization among threads to protect critical regions when you implement a server. However, you may wish to create threads of your own. For example, you might need a dedicated thread that responds to input from a user interface. And, if you have complex and long-running operations that can exploit parallelism, you might wish to use multiple threads for the implementation of that operation.
Ice provides a simple thread abstraction that permits you to write portable source code regardless of the native threading platform. This shields you from the native underlying thread APIs and guarantees uniform semantics regardless of your deployment platform.
27.11.1 The Thread Class
namespace IceUtil {
class Time;
class ThreadControl {
public:
#ifdef _WIN32
typedef DWORD ID;
#else
typedef pthread_t ID;
#endif
ThreadControl();
#ifdef _WIN32
ThreadControl(HANDLE, DWORD);
#else
ThreadControl(explicit pthread_t);
#endif
ID id() const;
void join();
void detach();
static void sleep(const Time&);
static void yield();
bool operator==(const ThreadControl&) const;
bool operator!=(const ThreadControl&) const;
};
class Thread {
public:
virtual void run() = 0;
ThreadControl start(size_t = 0);
ThreadControl getThreadControl() const;
bool isAlive() const;
bool operator==(const Thread&) const;
bool operator!=(const Thread&) const;
bool operator<(const Thread&) const;
};
typedef Handle<Thread> ThreadPtr;
}
The Thread class is an abstract base class with a pure virtual
run method. To create a thread, you must specialize the Thread class and implement the
run method (which becomes the starting stack frame for the new thread). Note that you must not allow any exceptions to escape from
run. The Ice run time installs an exception handler that calls
::std::terminate if
run terminates with an exception.
This method returns false before a thread’s start method has been called and after a thread’s
run method has completed; otherwise, while the thread is still running, it returns true.
isAlive is useful to implement a non-blocking join:
ThreadPtr p = new MyThread();
// ...
while(p‑>isAlive()) {
// Do something else...
}
t.join(); // Will not block
Note that IceUtil also defines the type
ThreadPtr. This is the usual reference-counted smart pointer (see
Section 6.14.6) to guarantee automatic clean‑up: the Thread destructor calls
delete this once its reference count drops to zero.
#include <IceUtil/Thread.h>
// ...
Queue q;
class ReaderThread : public IceUtil::Thread {
virtual void run() {
for (int i = 0; i < 100; ++i)
cout << q.get() << endl;
}
};
class WriterThread : public IceUtil::Thread {
virtual void run() {
for (int i = 0; i < 100; ++i)
q.put(i);
}
};
This code fragment defines two classes, ReaderThread and
WriterThread, that inherit from
IceUtil::Thread. Each class implements the pure virtual
run method it inherits from its base class. For this simple example, a writer thread places the numbers from 1 to 100 into an instance of the thread-safe
Queue class we defined in
Section 27.8, and a reader thread retrieves 100 numbers from the queue and prints them to
stdout.
Note that we assign the return value from new to a smart pointer of type
ThreadPtr. This ensures that we do not suffer a memory leak:
2
2.
Prior to calling run (which is called by the
start method),
start increments the reference count of the thread to 1.
3.
For each ThreadPtr for the thread, the reference count of the thread is incremented by 1, and for each
ThreadPtr that is destroyed, the reference count is decremented by 1.
4.
When run completes,
start decrements the reference count again and then checks its value: if the value is zero at this point, the
Thread object deallocates itself by calling
delete this; if the value is non-zero at this point, there are other smart pointers that reference this
Thread object and deletion happens when the last smart pointer goes out of scope.
Note that, for all this to work, you must allocate your
Thread objects on the heap—stack-allocated
Thread objects will result in deallocation errors:
ReaderThread thread;
IceUtil::ThreadPtr t = &thread; // Bad news!!!
This is wrong because the destructor of t will eventually call
delete, which has undefined behavior for a stack-allocated object.
Similarly, you must use a
ThreadPtr for an allocated thread. Do not attempt to explicitly delete a a thread:
Thread* t = new ReaderThread();
// ...
delete t; // Disaster!
It is legal for a thread to call start on itself from within its own constructor. However, if so, the thread must not be (very) short lived:
class ActiveObject : public Thread() {
public:
ActiveObject() {
start();
}
void done() {
getThreadControl().join();
}
virtual void run() {
// *Very* short lived...
}
};
typedef Handle<ActiveObject> ActiveObjectPtr;
// ...
ActiveObjectPtr ao = new ActiveObject;
With this code, it is possible for run to complete before the assignment to the smart pointer
ao completes; in that case,
start will call
delete this; before it returns and
ao ends up deleting an already-deleted object. However, note that this problem can arise only if
run is indeed
very short-lived and moreover, the scheduler allows the newly-created thread to run to completion before the assignment of the return value of
operator new to
ao takes place. This is highly unlikely to happen—if you are concerned about this scenario, do not call
start from within a thread’s own constructor. That way, the smart pointer is assigned first, and the thread started second (as in the example on
page 720), so the problem cannot arise.
27.11.4 The ThreadControl Class
The start method returns an object of type
ThreadControl (see
page 716). The member functions of
ThreadControl behave as follows:
The default constructor returns a ThreadControl object that refers to the calling thread. This allows you to get a handle to the current (calling) thread even if you do not have saved a handle to that thread previously. For example:
This example also explains why we have two classes, Thread and
ThreadControl: without a separate
ThreadControl, it would not be possible to obtain a handle to an arbitrary thread. (Note that this code works even if the calling thread was not created by the Ice run time; for example, you can create a
ThreadControl object for a thread that was created by the operating system.)
IceUtil::ThreadPtr t = new ReaderThread; // Create a thread
IceUtil::ThreadControl tc = t‑>start(); // Start it
tc.join(); // Wait for it
Note that the join method of a thread must be called from only one other thread, that is, only one thread can wait for another thread to terminate. Calling
join on a thread from more than one other thread has undefined behavior.
Calling join on a thread that was previously joined with or calling
join on a detached thread has undefined behavior.
Calling detach on an already detached thread, or calling
detach on a thread that was previously joined with has undefined behavior.
•
Do not call join on a thread from more than one other thread.
•
Do not leave main until all other threads you have created have terminated.
•
A common mistake is to call yield from within a critical region. Doing so is usually pointless because the call to
yield will look for another thread that can be run but, when that thread is run, it will most likely try to enter the critical region that is held by the yielding thread and go to sleep again. At best, this achieves nothing and, at worst, it causes many additional context switches for no gain.
If you call yield, do so only in circumstances where there is at least a fair chance that another thread will actually be able to run and do something useful.
Following is a small example that uses the Queue class we defined in
Section 27.8. We create five writer and five reader threads. The writer threads each deposit 100 numbers into the queue, and the reader threads each retrieve 100 numbers and print them to
stdout:
#include <vector>
#include <IceUtil/Thread.h>
// ...
Queue q;
class ReaderThread : public IceUtil::Thread {
virtual void run() {
for (int i = 0; i < 100; ++i)
cout << q.get() << endl;
}
};
class WriterThread : public IceUtil::Thread {
virtual void run() {
for (int i = 0; i < 100; ++i)
q.put(i);
}
};
int
main()
{
vector<IceUtil::ThreadControl> threads;
int i;
// Create five reader threads and start them
//
for (i = 0; i < 5; ++i) {
IceUtil::ThreadPtr t = new ReaderThread;
threads.push_back(t‑>start());
}
// Create five writer threads and start them
//
for (i = 0; i < 5; ++i) {
IceUtil::ThreadPtr t = new WriterThread;
threads.push_back(t‑>start());
}
// Wait for all threads to finish
//
for (vector<IceUtil::ThreadControl>::iterator i
= threads.begin(); i != threads.end(); ++i) {
i‑>join();
}
}
The code uses the threads variable, of type
vector<IceUtil::ThreadControl> to keep track of the created threads. The code creates five reader and five writer threads, storing the
ThreadControl object for each thread in the
threads vector. Once all the threads are created and running, the code joins with each thread before returning from
main.
Note that you must not leave
main without first joining with the threads you have created: many threading libraries crash if you return from
main with other threads still running. (This is also the reason why you must not terminate a program without first calling
Communicator::destroy (see
page 262); the
destroy implementation joins with all outstanding threads before it returns.)