[cpp-threads] Thread API interface tweaks

Howard Hinnant hinnant at twcny.rr.com
Sun Aug 27 02:37:37 BST 2006


Based on the feedback from the Redmond meeting, here are some changes  
I'm thinking about regarding thread launching (I'm also thinking  
about changes for locks, but that is not reflected herein).

One of the big questions was whether or not joiners should be  
copyable or move-only.  I'm a big fan of move-only because of the one- 
to-one correspondence between a thread-object and the thread of  
execution (much like between a file-object and a file, or unique_ptr  
and the pointer it owns).  This one-to-one correspondence also leads  
to an extremely simple and efficient implementation over the OS  
services (prototyped on pthreads).

However Herb spoke eloquently of his "futures" view of threads where  
one regards the future as merely a reference to the return value of  
the thread, and not to the thread itself.  In this viewpoint, the  
reference is logically copyable with reference semantics.  After an  
extremely productive 45-second conversation between Herb and myself  
we came up with the possibility of a layered approach where the  
client might have the option for both view points using different types.

On the flight home I prototyped this idea, and included a couple of  
name changes as well (The name "joiner" confused more than one  
person).  Here's what I came up with, comments encouraged:

// A handle to a thread of execution
// Can get a thread id from it, while it is executing, otherwise  
converts to "not a thread" id
// Movable, but not copyable
template <class Return>
class thread
{
private:
     thread(const thread&);
     thread& operator=(const thread&);
public:
     thread();   // refers to no thread
     ~thread();  // detaches if this refers to a thread

     thread(thread&& x);             // this refers to what x did  
refer to
     thread& operator=(thread&& x);  // this refers to what x did  
refer to

     Return operator()();  // blocks/joins with thread, throws if  
thread throws
     bool joinable() const; // true if this refers to a not-yet- 
joined thread of execution
};

"thread" is "joiner" renamed.  thread controls the ability to join  
with, or detach a thread.  Only one thread ever refers to a thread of  
execution.  Thus it is not possible for more than one thread to join  
with or detach the thread of execution (but see "future" below).   
Although the interface is identical, a likely implementation  
technique will be to specialize thread<void> for efficiency reasons  
since there is no reason to create a spot on the heap for the return  
type (not yet sure if Beman's exception propagation idea will mandate  
that storage anyway).

This thread<void> is nearly identical to boost::thread (boost::thread  
will launch itself and isn't movable).

// Factory class for thread
// Copyable
class thread_launcher
{
public:
     template <class F>
         thread<typename std::tr1::result_of<F()>::type> operator()(F  
f);
};

// Factory function for thread
template <class F>
thread<typename std::tr1::result_of<F()>::type>
launch_thread(F f);

Factory function and factory class for launching a thread.  These  
were called threader and thread respectively.  thread_launcher may  
contain implementation defined data such as thread priority or stack  
size (set by implementation defined constructors).

// Just a thread id, does not reference a thread
// Copyable
class thread_id
{
public:
     thread_id();                   // not a thread_id
     static thread_id current();    // current thread_id

     template <class R> thread_id(const thread<R>& j);  //get id of  
thread
     pthread_t native() const;  // return type implementation defined

     friend bool operator==(const thread_id& x, const thread_id& y);
     friend bool operator!=(const thread_id& x, const thread_id& y);

     static void yield();                          // operate on this  
thread
     static void sleep(const timespec& rel_time);  // operate on this  
thread
     static unsigned n_cpus();  // number of cpu's, valuable for  
building thread pools
};

thread_id is simply a way to store the id of a thread.  One can  
construct a thread_id from a joinable "thread".  Once the thread  
detaches or is joined with, the thread_id will still contain the  
original thread id (it has value semantics, not reference  
semantics).  However getting the thread_id of a thread which has  
already been joined with will result in a thread_id equal to a  
default constructed thread_id, which represents "not a thread".   
There's a way to extract the native thread id type so that OS  
functionality not wrapped by this library can be accessed.

// Refers to the return value of a thread, but not to the thread itself
//  (you can't get a thread id from it)
// Copyable
// Not default constructible, always refers to a thread result
// future<void> does not compile
template <class Return>
class future
{
public:
     future(const thread<Return>& j);
     future(const future& f);
     future& operator=(const future& f);
     ~future();

     bool is_done() const;       // thread has completed (perhaps  
abnormally)
     bool is_normal() const;     // A return value is available
     Return operator()() const;  // throws if is_done() && !is_normal().
};                              //    Blocks if !is_done().

The difference between a future and a thread is that the future  
refers only to the return value and not to the thread of execution.   
One can not get a thread_id of a future.  The future is copyable,  
where the thread is move-only.  The future destructor does not detach  
the thread of execution as the thread destructor does.  The future  
can block on the return value if it is not yet available, but it is  
signaled via mutex/condition variables upon thread completion instead  
of calling something like pthread_join.  There are observers to tell  
if the thread has completed, and if so, whether it was a normal or  
abnormal termination.  A value can be extracted repeatedly from the  
same future.  In contrast a value can only be extracted once from a  
thread (after which the thread no longer refers to a joinable thread,  
or to a return value).

This design means that a future only keeps the return-value struct  
alive.  The thread of execution, and all of its associated OS  
resources are cleaned up on thread join, or upon termination of a  
detached thread.

Although a thread<void> is quite functional, a future<void> is  
senseless and should not compile (diagnostic required).

The return value is a structure on the heap.  Its ownership is shared  
by the thread start function (a wrapper for what the client views as  
the thread function), the single joiner, which may destruct right  
after thread launch, or be moved from one scope to another, and zero  
or more futures which can be implicitly constructed from a joinable  
joiner, and copied from each other.

The cost (that I've discovered so far) of supporting this "future"  
design, even if a future is not instantiated is an extra pair<mutex,  
condition<mutex>>* (pointer) in the return value structure which can  
remain null if no futures are created from the thread which owns the  
return value.  Another cost is that the return value of a thread must  
be copyable.  If, when the value is requested from a thread, there  
are no futures sharing ownership of the return value, then the thread  
can return with move construction.  Otherwise it must return by copy  
construction.  And in any event this information isn't known until  
run time so move-only types (streams, unique_ptr, locks, ect.) can  
not be returned by threads/futures (a regrettable cost).

Perhaps if concepts give us a way to detect movable but non-copyable  
types, then thread could be specialized on such types, and futures of  
such types would simply fail to compile.  Thus you could have  
thread<ofstream>, and move it from the return of the thread, but not  
future<ofstream> (since futures by their nature demand copyable).

Example code snippets:

string f(int i);
void g();

thread<string> j1 = launch_thread(bind(f, 1));  // run attached  
thread and join to it with j1() later
thread_id id = j1;  // get thread id of f(1)
assert(id == j1);  // id is equal to the thread_id of j1
assert(id != thread_id());  // id is a valid thread_id
thread<string> j2 = j1;   // doesn't compile, only one thread<string>  
can refer to f(1)
thread<string> j2 = move(j1);   // ok, j2 refers to f(1) and j1 no  
longer does
...
future<string> f1 = launch_thread(bind(f, 1));  // run detached  
thread, retain shared ownership only to its return value
thread_id id = f1;  // doesn't compile, there's no thread to get an  
id of
future<string> f2 = f1;  // ok, f2 and f1 share ownership of the  
return value of f(1), but not the f(1) thread itself
...
thread<void> g_thread = launch_thread(g);  // ok, g_thread owns the  
thread g()
g_thread();  // block/join with g()
...
future<void> g_thread = launch_thread(g);  // doesn't compile, there  
is no such thing as future<void>

-Howard




More information about the cpp-threads mailing list