[cpp-threads] Re: Thread API interface tweaks

Kevlin Henney kevlin at curbralan.com
Sun Aug 27 18:19:54 BST 2006


Howard Hinnant <hinnant at twcny.rr.com> writes
>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.

This seems to be a good resolution and a nice fit between the two models 
-- a fit I wish I'd been able to see before! It makes the best use of 
move semantics and a lightweight implementation for the core concept.

>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).

I was never unconditionally wed to the name "joiner", but I had been 
hoping to avoid "thread". No matter.

>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
>};

Looks good, although I favoured a Boolean UDC for checking joinability.

>class thread_launcher
>{
>public:
>    template <class F>
>        thread<typename std::tr1::result_of<F()>::type> operator()(F 
>f);
>};
>
>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).

Yup, these look good, and the renaming seems fine.

>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".

In my original design, the idea of a thread ID was more opaque and was 
guaranteed only to support == and !=, although the idea was that it 
could in principle offer a platform backdoor. I think it is a good idea 
to formalise it as you have, but I think that it is perhaps doing too 
much work, ie it seems to embrace multiple responsibilities.

There is the idea of a thread ID and then there is the idea of yield, 
sleep, etc. There is no reason that they should be related in the same 
class, and some good reasons not to. These current-thread properties and 
manipulators would probably be better off as non-member functions 
(perhaps nested in a namespace) as they are not operations on ID or a 
way of getting low-level access to an ID. Does that sound reasonable?

>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.

This makes a lot of sense.

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

However, I disagree with this. A future<void> still represents a 
synchronisation mechanism and permitting avoids making an exception that 
will undoubtedly trip up some generic code somewhere -- and therefore 
cause us to repeal the omission of future<void> :-)

Anyway, overall this looks great!

Kevlin
-- 
____________________________________________________________

   Kevlin Henney                   phone:  +44 117 942 2990
   mailto:kevlin at curbralan.com     mobile: +44 7801 073 508
   http://www.curbralan.com        fax:    +44 870 052 2289
   Curbralan: Consultancy + Training + Development + Review
____________________________________________________________




More information about the cpp-threads mailing list