[cpp-threads] Thread API interface tweaks

Herb Sutter hsutter at microsoft.com
Sun Aug 27 06:32:55 BST 2006


Thanks, Howard! Quick partial response:

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

Actually I think future<void> is quite useful, though I admit I'm mostly thinking of designs where the future<T> is both a future value (primarily) and a "handle" to the asynchronous operation that is responsible for generating the future value (not necessarily "thread"). At least in such designs, people want to be able to do things like ask if the operation is complete (which your future<T> already has, below), to request to cancel it, etc.

Even if we didn't have a use for future<void>, I would worry about arbitrarily disallowing it, and creating a special case ("future<T> is for any return type, oh, well, except void"). For example, in templates wouldn't return future<void> come up just like return void()? The historical 'return void' motivation was to permit code like the following to compile:

  template <class T>
  T DoSomethingAndInvoke( T (*pf)() ) {
    ...
    return pf();
  }

  int g();
  void h();

  Invoke( g ); // ok
  Invoke( h ); // ok because of the 'return void' latitude

Now consider:

  template <class T>
  future<T> AsyncDoSomethingAndInvoke( T (*pf)() ) {
    ...
    return thread_launcher()( pf );
  }

(Aside: I'm not sure if I got your launcher syntax right here, but at brief glance I think you intend the above terse return statement to work without explicitly converting through thread<T>. Right?)

Anyway, that's the idea. Thoughts?

Herb



> -----Original Message-----
> From: cpp-threads-bounces at decadentplace.org.uk [mailto:cpp-threads-
> bounces at decadentplace.org.uk] On Behalf Of Howard Hinnant
> Sent: Saturday, August 26, 2006 6:38 PM
> To: Hans Boehm; Robison, Arch; Martin Sebor; C++ threads standardisation;
> David Miller; Lawrence Crowl; jerry at acm.org; Terrence.Miller at Sun.COM;
> David Miller; Mattson, Timothy G; Bill Seymour; Kevlin Henney; Ion
> Gaztañaga; P.J. Plauger (Dinkumware Ltd); Bjarne Stroustrup; Alisdair
> Meredith; Eric Niebler; Pete Becker; Bronis R. de Supinski
> Subject: [cpp-threads] Thread API interface tweaks
>
> 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
>
>
> --
> cpp-threads mailing list
> cpp-threads at decadentplace.org.uk
> http://www.decadentplace.org.uk/cgi-bin/mailman/listinfo/cpp-threads



More information about the cpp-threads mailing list