[cpp-threads] RE: "Agenda" for august 23-25 concurrency meeting

Howard Hinnant hinnant at twcny.rr.com
Thu Aug 31 21:48:32 BST 2006


On Aug 31, 2006, at 3:36 PM, Peter Dimov wrote:

>>> Is
>>>
>>> future< vector<int> > fv = ex.execute( bind( compute, input  
>>> data ) );
>>> vector<int> v = fv.move(); // second move() or subsequent op()
>>> throws value_already_moved
>>>
>>> good enough? It'd work for any asynchronous function call, not just
>>> a dedicated thread, but the syntax is a bit clumsier (and the
>>> implementation somewhat fragile but doable.)
>>
>> This design turns what I proposed to make a compile time error into a
>> run time error.
>
> I must be missing something.
>
> thread<vector<int> > t = launch_thread(bind(compute, input_data));
> vector<int> v = t();
> vector<int> v2 = t(); // ?
>
> How can you make the third line a compile-time error, but not the  
> second?

Sorry, my bad.  I rewrote my reply several times and my final version  
was both unclear and incorrect.

Yes, what you show is a run time error too.  My train of thought went  
something like:

If I'm using future<T> I may or may not be using a design philosophy  
of single ownership of the return value (so that I can move out of  
it).  Upon inspecting code (that perhaps I'm unfamiliar with) I need  
to search for use of future<T>::move, and also search for multiple op 
() calls to a future, *and* any of that future's copies, to ensure  
that the code is well behaved.

If I'm using thread<T>, I know I've got unique ownership of the  
return value.  I should only see use of op() in one place.  If I see  
op() on a thread more than once, or from a moved-from thread, I know  
immediately I've got a mistake and don't have to further revisit the  
design of this part of the code.  I.e. sometimes a shared_ptr is an  
appropriate design, and sometimes a unique_ptr (auto_ptr) is  
appropriate.  And having two types to serve the two designs is more  
likely to result in easily understandable code.  Additionally, the  
fact that a thread isn't copyable means accidental copies will be  
caught at compile time (that's why unique_ptr is better than auto_ptr):

thread<R> t = ...
generic_func(t);  // oops, meant move(t), error flagged by compiler
---
fixed:
thread<R> t = ...
generic_func(std::move(t));
// it is now more obvious that t() here would be a mistake

Contrast with:

future<R> t = ...
generic_func(t);
// Did generic_func steal my return value?

That's not to say you shouldn't send a future to a generic function.   
Indeed I'm sure that such use will be very useful.  It's just to say  
that sometimes you need a shared_ptr and sometimes you need a  
unique_ptr. :-)  And I think it would be error prone to combine those  
two into one type that sometimes displayed shared ownership and  
sometimes unique ownership.

>
>> It also destroys efficiency for generic code getting
>> an asynchronous value.
>>
>> template <class T, class Pred>
>> void foo(Pred p)
>> {
>>    ...
>>    T temp = p();  // if asynchronous, inefficiency mandated?
>> // or:
>>    T temp = p().move();  // No longer generic, Pred must be a future
>>    ...
>> }
>
> Agreed... but...
>
> template <class T, class Pred>
> void foo(Pred p)
> {
>    ...
>    T temp = p();
>    T temp2 = p(); // ?
> }
>
> This code is fine when p is a future.

Yes.  If generic code is known to evaluate the functor more than once  
(like std::sort does with the Compare), it would be inappropriate to  
send it a thread (and concepts might even catch such an error, not  
sure).

>> One can never build efficient code on top of an inefficient base.  If
>> there is no efficient layer in the threading API, then we have made
>> the performance/feature tradeoff decision for all C++ clients in this
>> area.  I'd far rather provide only an efficient base layer upon which
>> clients can build their own feature-rich code, than provide only a
>> feature-rich (but expensive) API.  If we can provide both layers,
>> great (and I think we can).  But there is no question which layer is
>> more important for us to provide (imho).
>
> template<class R> class thread
> {
> private:
>
>    future<R> ft_;
>
> public:
>
>    template<class F> explicit thread( F f )
>    {
>        ft_ = threaded_executor().execute( f );
>    }
>
>    R operator()()
>    {
>        return ft_.move();
>    }
> };
>
> ?

Right, now we have an efficient *interface*.  The fact that it was  
implemented inefficiently is not a standards issue. ;-)

> Also, the inefficiency does not manifest itself that often. If you  
> have a
>
> vector< thread< vector<int> > > v;
>
> , fill it with async calls and then retrieve the return values in
>
> vector< vector<int> > v2;
>
> you only gain when you later need to operate on v2 in place.

v2 as a whole is movable, as is each individual element.  I.e.  
vector< vector<int> > is as efficient, or as expensive as the client  
needs it to be.

> In the immutable case,
>
> vector< future< vector<int> > > v;
>
> (with the R const& return modification) is as efficient as the  
> above, since the results are contained within the futures and there  
> is no need to copy them out.

I'm all for the R const& return from future as it provides exactly  
this option (of zero-expense const viewing).  Note here though that  
you are either limited to that const view, or you need to pay for a  
copy (which is fine if that is by your application's design).

> There's also the thread creation overhead that can easily dominate  
> the vector<int> copy, but I'll be willing to assume for the sake of  
> the argument that the underlying library pools the threads and the  
> vectors are sufficienty large. :-)

Good.  The copy of a return can be arbitrarily expensive.

>
> I never explored the possibility of a noncopyable but movable  
> future, by the way.

In a nutshell, this is what I'm after, but under a different name.

I want to make:

T foo();

callable either synchronously or asynchronously with the same  
complexity cost, even if T is a heavy container.  It would be a shame  
if we brought threads to C++ (in the name of efficiency to make use  
of all those processors) and then made them more difficult than need  
be to use efficiently.  futures are good too.  But they are a layer/ 
level up from where I'm concerned.

-Howard




More information about the cpp-threads mailing list