Boost logo

Boost :

From: William Kempf (williamkempf_at_[hidden])
Date: 2001-08-17 08:05:14


We're nearing the date slotted for submission of Boost.Threads, but there
are still a couple of design issues that I need to work out. So, I'm coming
here one final time for opinions. I *think* I know what to do in each case,
but I want to see if I'm missing something.

The first is the thread specific storage concept. My final design is the
following:

template <typename T>
class thread_specific_ptr : private noncopyable
{
public:
    thread_specific_ptr();

    T* get() const;
    T* operator->() const;
    T& operator*() const;
    T* release();
    void reset(T* p=0);
};

This is similar to std::auto_ptr<> in it's interface, though the type is
non-copyable. When a thread is created it's thread specific instance is
initialized to 0, so the first time get() is called it will return 0. For
this reason calling operator->() and operator*() may be dangerous. Given
this and the fact that these operations are expensive enough that most
usages will probably use get() instead and cache the value, I'm wondering if
the smart pointer operations should even be included?

The release() and reset() methods allow the programmer to decide whether or
not thread specific data is stored on the heap or not since the following
pattern can be used (which could be wrapped in a RAII class):

   static thread_specific_ptr<thread_specific_data> tsp;

   void my_thread_func()
   {
      thread_specific_data data;
      tsp.reset(&data);
      try
      {
         // thread functionality
      }
      catch (...)
      {
         // In case it's the theoretical thread_cancellation exception
         tsp.release();
      }
      tsp.release();
   }

So, this actually achieves all of the goals I discussed in an earlier
posting, though this interface is decidedly different from the usual C APIs,
and even of the "Thread Specific Proxy" pattern typically employed in C++.

The next design decision is the semantics for thread::join(). Alexander
Terekhov had a lot of enlightening things to say in
http://groups.yahoo.com/group/boost/message/16024. I'll quote him in my
discussion here.

(Alexander quoting someone else) "In fact, nobody could ever construct a
case where it made sense for both threads 1 and 2 to wait for thread 3 to
complete. While there might be cases where thread 3 was doing something on
which two threads depended, joining with it is never the right (or best) way
to implement that dependency. (It really depends on some DATA or STATE, not
on the completion of the thread, per se.) And it's silly to require two
calls to "finalize" thread 3. Furthermore, one of the most common DCE thread
programming errors was failure to detach after joining, resulting in a
memory leak."

In Win32 MT programming I've frequently waited on a thread handle multiple
times. Naively, I assumed this was functionality that was needed by
Boost.Threads because of this experience. However, once pointed out I must
agree... these were all cases in which I was more interested in some state
rather in the actual "full termination" of the thread, including cleanup
procedures. There's only ever one thread (usually the main thread of
execution) that must wait for "full termination" in order to insure all
cleanup occurs. The other cases could more effectively be handled with
condition variables instead of a call to join(). Further, since allowing
multiple calls to join() from a pthreads implementation is going to be very
problematic I'm now leaning towards following a slightly more POSIX stance
on this. (Not too surprising... POSIX *is* a full standard and as such
great attention has been paid to some of these details.)

"so taking into account that "detach" applied to already terminated thread
("DCE-joined" thread) is nothing else but simply a request to reclaim
resources (thread id/native thread object), i would strongly suggest that it
should be done automatically on destruction of C++ thread object which
refers to not detached/"joinable" thread (after waiting until the target is
complete if it was not already done "manually" using some thread::XXXX
method(s)). in the case of detached thread, C++ thread object should be
destructed implicitly on thread termination (inside thread bootstrap routine
or via tsd destruction mechanism) followed by a request to reclaim thread
native resources (id/native object) using pthread_detach( pthread_self() );"

This I don't agree with. The join() method is a cancellation point (or will
be if/when we support cancellation) and so could erroneously result in
program termination if called during stack unwinding caused by another
exception being thrown. This one can't be worked around by disabling
cancellation in the destructor as other cancellations can be. I'd prefer to
leave join() as an explicit operation. The destructor shall either detach
the thread if it is still "joinable", or do nothing if it's been joined.
This also means that timed_join() and try_join() must go, since POSIX
doesn't support such a concept and there's no way to extend Boost.Threads
to. Further, it means that I'm even more adamant that the name remains
join() and not await_completion() because there are some semantics involved
here that go beyond simply waiting for the thread to terminate. I think
Alexander tried to point this out a few other times but I didn't comprehend
what he meant until now. A call to join() not only waits for the thread to
complete, but also performs any cleanup of implementation state required,
including returning the "thread id" to the system for reuse.

So, I'd like to modify the thread concept in the following ways:

Constructors

thread();

Effects: Constructs a thread object for the current thread of execution.

Notes: *this is not joinable.

thread(const boost::function0<void>& threadfunc);

Effects: Starts a new thread of execution. Copies threadfunc (which in turn
copies the function object wrapped by threadfunc) to an internal location
which persists for the lifetime of the new thread of execution. Calls
operator() on the copy of the threadfunc function object in the new thread
of execution.

Throws: boost::thread_resource_error if a new thread of execution cannot be
started.

Notes: *this is joinable.

Join

void join();

Requires: *this != thread() and *this is joinable.

Effects: The current thread of execution blocks until the initial function
of the thread of execution represented by *this finishes. Insures all
resources used by the thread have been reclaimed.

Notes: *this is no longer joinable. After return no thread objects
referring to the same thread of execution are valid any longer, and the only
operation that's well defined for any of them is destruction.

Comments?

Bill Kempf

_________________________________________________________________
Get your FREE download of MSN Explorer at http://explorer.msn.com/intl.asp


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk