Other articles will disccuss porting the library, as well as using the higer level constructs it provides.
Once this kind of support is available, much more time can be spent on evolving design patterns for using multi threaded. Developing new patterns, such as the Executor and Future patterns shoudl be beneficial to everyone.
Should C++ have built in support for threads? I'm not sure that is completely necessary. What would go a long way towards improving support for code that threads would be providing a way to deal with related issues, such as a few standard atomic operations (TAS, CAS, DCAS, ...) and perhaps chaning volatile or mutable to ask the compiler to insert a memory barrier. Something like that, coupled with a way to handle the order certain objects are destroyed in (a more flexible atexit() function) would add alot of power to the language and to what you can do with threads without having to pick any one model for multithreading. This library demonstrates that flexible, portable support for threads, including a cancellation-like mechanism can be built on top of current thread models.
|
Cancellation is a C based mechanism; because of this it does not safely
interact with exceptions. A thread cancelled within a try block will not
neccessarily unwind the stack correctly; this makes it extremely difficult to write
safe code that uses cancellation with C++ exceptions.
This library is intended to be object-oriented. Pushing function pointers onto a clean up stack can be somewhat awkward to incorporate nicely into an object-oriented design. |
|
Cancellation is not completely portable. Although it is standard, different
operating systems have different levels of support for this mechanism. Some
cancellation points are optional, and some cancelltions are non-standard.
When writing code for a single system it is not at all difficult to lookup the supported cancellation points. However, writing portable code would require finding out about each platforms cancellation support. This is tedious at best. If this is not done correctly it is very possible to write code on one system that does not behave the same way on another system, or to write code that relies on a cancellation that exists on one system, but not on another. It is possible to turn cancellation on and off for various regions of code, but this really leads to the same problem. A programmer can easily forget to do this and get the expected behavior on one system only to find that porting another system has brought about a series of unexpected cancellation problems. This library is all about abstraction and portability. An important goal is to remove the burden of dealing with the specific details of each system from the programmer. |
|
Cancellation is an invasive mechansim. Invoking the cancellation on a thread
has the potentialy to disturb non-pthreads functions. This is by design
and again is not an issue when writing code for one system. However, it
becomes a tricky issue when writing portable code.
Emulating this kind of behavior would require creating wrappers for functions that should act as cancellation points for systems that don't support POSIX cancellation (Win32). This in completely outside the scope of a thread library. This libraries focus is on portable threads, not on creating a portable version of libc that supports cancellation. Additionally, the I/O models uses by different systems are very different. For example, Win32 uses compeletion ports, event handles, winsock, etc. It is not possible to choose a single set of functions that are appropriate cancellation points for all systems because of these differences. |
| WaitForMultipleObjects(), like POSIX cancellation, is also not C++ exception based. Although it does not have the same problems in interacting with exception based code, it can still be a bit more awkward to deal with error codes and switch statements. |
|
WaitForMultipleObjects() is based on Windows handles. It is very versatile
because it allows you to wait on a set of handles and to the end that wait
when any of those handles is signaled. Just about everything in Windows
uses handles making it possible to do very interesting things like wait on
thread handle, a socket handle, a file handle and an event handle.
The main drawback is that on other systems not everything uses the same opaque type to create generic handles. For example, on POSIX file handles (int), thread handles (pthread_t) and mutex handles (pthread_mutex_t) are unrelated. It very difficult to create a POSIX implementation of that function with the same versatility. Without the ability to wait on several kinds of handles a big part of the usefulness of WaitForMultipleObjects() would be lost. |
| Another difficulty in using this type of approach is that, just like POSIX cancelation, it can be easy to misuse. Programmers must be very aware of the consequences involved in using event handles correctly. While this can be chalked up to programmer responsibility, this library is an abstraction. Its intended to take the focus away from these lower level details. |
To deal with these problems some type of solution was needed. First and foremost, this solution should be 100% portable. The primary goal is portability and abstraction. Remember that this library is not intended to replace pthreads or win32 threads or any operating systems native thread support. Its meant to take advantage of that support and to extend it to provide a truely portable abstraction for using threads.
| Interruption is a completely C++ exception based mechanism. Stack unwinding is done correctly, and there are no issues involved with using interruption safely. Its very natural to use interruption if you are used to writing exception based code. No function pointers are required. |
| Interruption is a completely portable mechanism. There are no issues involved with unexpected behavior when porting interruption-based code to another system. A program using interruption will remain consistent n its behavior. |
|
Interruption is far more flexible. Cancellation is a terminal operation, while
interruption is not. Cancellation has cancel-and-exit semantics, which is
to say that once a thread is cancelled it has to die. This can be limiting in
what can be accomplished.
Cancellation was not intended to be a communication mechanism. Interruption, on the other hand, is temporary. A thread will recieve an exception when interrupted, but it will reset to a normal state once that exception is thrown; it is not forced to terminate. The thread can go on to do other things, which adds a huge amount of flexibility to what can be accomplished. If a programmer prefers to use cancel-and-exit semantics, they can take an exception thrown as a result of interruption to mean they need to shut down and exit. However, if they want to expliot the flexiblity of interruption for other purposes, that can also be accomplished. Interruption is an important communication mechanism. |
|
Interruption is not an invasive mechanism. Interruption is focused on threads
and thread related operations. by default it will not affect functions outside
of the ZThreads library. This is important for portability.
However, interruption does provide a hook that lets a programmer tie some system specific behavior to he interruption mechanism. This can be used to make interruption have some effect on functions external to the ZThreads library if this is desired. |
This is the only, and first, free library that provides this kind of portable C++ support.
To address this, killing a thread has been altered to act as a specialized form of interruption. Like interruption, killing a thread will produce an exception in an executing thread that is killed. Also, like interruption, after this exception is thrown the thread will continue to act normally. The difference is that a thread will always report that it has been killed (with the isKilled() method).
This method for killing threads allows the thread to cleanup and exit keeps the state of the program as consistent as possible. There is no danger of leaving the program in an inconsistent state. Which should lead to much more stable programs.
Daemon threads are also quite useful in creatng more flexible code. The Executor implementation is one example of the flexibility that can be achieved with this kind of mechanism.
A program will wait for all daemon threads to exit before it exits.
The following code shows how a normal thread might be used.
class Task : public Runnable {
public:
virtual ~Task() throw() { }
virtual void run() throw() {
// Do something
}
};
void func() {
Task job;
Thread t1;
t1.run(job); // Pass by reference, does not transfer ownership
t1.join();
}
In this example, the Task object, job, lives inside the scope of func().
The thread is join()ed before the scope is left, the job object will not
be destroyed before its used. Now, if we change func() slightly,
void func() {
Task job;
Thread t1;
t1.setDaemon(true);
t1.run(job); // Pass by reference, does not transfer ownership
}
t1 becomes a daemon thread. This thread does not have to be joined, that
will be taken care of automatically. The problem is that t1 may continue executing
after func() exits. The Task object, job will be destroyed when that happens
and that will be a big problem. This is where optional move semantics come into
play.
void func() {
Task job;
Thread t1;
t1.setDaemon(true);
t1.run(&job); // Pass by pointer, _does_ transfer ownership
}
By passing the Task object by pointer, rather than by reference, we transfer
(or move) the ownership of that object to the thread. The thread will
delete that task before it exits. By including both options, both
specific tasks for specific threads and shared tasks for many threads
can be supported.
The synchronization objects are designed to be used as members of objects. They have a clear and unique identity because they can not be copied. This eliminates confusion revolving around which object is really getting locked.
class SharedObject {
Mutex lock;
public:
void func() {
Guard g(lock);
}
};
This is very fitting for an object-orientented library.
Thread objects are NonCopyable as well. They can be allocated on the local stack or on the heap. Making these objects NonCopyable encourages the correct usage of these objects and it simplifies extending the Thread class. For example,
class MyThread : public Thread {
Data _someData;
public:
// ...
virtual void run() throw() {
// do something with _someData
}
};
If the Thread class weren't NonCopyable, several instances of this class could exist
at the time. Now, when one of these instances is start()ed the user is going to have
are real hard time trying figure out which instance of _someData is actually accessible
from the other thread of execution. Making a Thread NonCopyable eliminates this problem.
// Create a thread
void func() {
Task aRunnable;
Thread t;
t.run(aRunnable);
t.join();
}
// Create a group of threads
void func() {
Task aRunnable;
Thread t[10];
for(int i=0; i<10; ++i)
t[i].run(aRunnable);
for(int i=0; i<10; ++i)
t[i].join();
}
// Create a group of threads, assign ownership to something else
void func() {
Task aRunnable;
std::auto_ptr t[10];
for(int i=0; i<10; ++i)
t[i] = new Thread;
for(int i=0; i<10; ++i)
t[i]->run(aRunnable);
for(int i=0; i<10; ++i)
t[i]->join();
}
void func() {
Task* aRunnable = new Task();
Thread t;
t.setDaemon(true);
t.run(aRunnable);
}
| Copyright © 2000 - 2002, Eric Crahen <crahen at cs dot buffalo dot edu> - All rights reserved. |
| Permission to use, copy, modify, distribute and sell this documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. Eric Crahen makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. |