[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