future/packaged_task Sean Parent design
Hi,
Sean Parent suggests (See [1] Better Code: Concurrency) to remove the
promise and left the packaged_task serve as single provider of a future.
Both are created at once using the package factory.
His future library doesn't include a shared_future as the future
contains a value that can be observed several times.
The continuation takes the value and not the future. So a future can
have several continuations. If the store type is movable only, then the
future can have only one continuation.
Not really strange coming from Sean, future and packaged_task are
copyables :)
One more thing, there is not get no wait, but a get_try that return an
optional<T>.
Scales better. IIUC the implementation allows to implement when_all on
top of future/packaged_task. The trick is that that the packaged_task
can store some context.
async has always an Scheduler as parameter.
Scheculers are just Copyable Callables having this signature
void(void()). There is no need for function add/spawn.
when_all has also a callable as argument.
I find the Sean's design clean and simple (See the code [2]). What do
you think of it? Do you see any issues on it?
* I don't understand why when_all continuation has the futures as
parameters (I suspect that this is a type on the documentation).
* I would rename get_try to try_get. This function could return
optional
On Wed, Jun 24, 2015 at 12:09 AM, Vicente J. Botet Escriba
Hi,
Sean Parent suggests (See [1] Better Code: Concurrency) to remove the promise and left the packaged_task serve as single provider of a future. Both are created at once using the package factory.
Yes, packaged task is a far better producer side interface than a naked promise. I like the idea of creating the future and promise (or better, packaged_task) together, although my preference would be a way to create the packaged task from the future, which opens a lot of optimiation opportunities (for example if the future is non copyable non movable, no memory allocation is required).
His future library doesn't include a shared_future as the future contains a value that can be observed several times.
This is my biggest issue with Sean's design. Sharing the state among multiple consumes pretty much requires heavy weight synchronization. But see below.
The continuation takes the value and not the future. So a future can have several continuations.
What's the thread safety guarantees of future? 'As safe as int' or, for example, concurrent calls to .then are allowed? If the former, a copy could be a real deep copy (a new shared state is allocated and broadcasting is internally done via 'then'). That's not how Sean future is currently implemented though.
If the store type is movable only, then the future can have only one continuation. Not really strange coming from Sean, future and packaged_task are copyables :) One more thing, there is not get no wait, but a get_try that return an optional<T>.
Intresting, so if a consumer needs wait must implement it on top of then. This is fine, but you need a way to unregister a 'then'.
Scales better.
the interface is certanly nicer and simpler, but why do you think it scales better? [...]
What are you missing ? [...]
The when_any continuation probably needs to be passed the all the futures instead of just the value that is ready, so that it can be called recursively. when_any also requires the ability to unregister a continuation from a future, otherwise if called 'in a loop' the number of continuations associated with futures will grow. Implementing this eficiently is not trivial. -- gpd
Le 24/06/15 02:18, Giovanni Piero Deretta a écrit :
On Wed, Jun 24, 2015 at 12:09 AM, Vicente J. Botet Escriba
wrote: Hi,
Sean Parent suggests (See [1] Better Code: Concurrency) to remove the promise and left the packaged_task serve as single provider of a future. Both are created at once using the package factory.
Yes, packaged task is a far better producer side interface than a naked promise.
I like the idea of creating the future and promise (or better, packaged_task) together, although my preference would be a way to create the packaged task from the future, Yes, I don't see any major problem with this approach. future will then be default constructible also. which opens a lot of optimiation opportunities (for example if the future is non copyable non movable, no memory allocation is required). Sean' future is copyable and movable. Do you mean if the stored value? Are thinking of the future<void> specialization? When no memory allocation will be needed?
His future library doesn't include a shared_future as the future contains a value that can be observed several times. This is my biggest issue with Sean's design. Sharing the state among multiple consumes pretty much requires heavy weight synchronization. Could you elaborate? It this different for std::shared_future? But see below.
The continuation takes the value and not the future. So a future can have several continuations. What's the thread safety guarantees of future? 'As safe as int' or, for example, concurrent calls to .then are allowed? If the former, a copy could be a real deep copy (a new shared state is allocated and broadcasting is internally done via 'then'). That's not how Sean future is currently implemented though. Of course, concurrent calls to .then must be allowed. I don't see when would you need the copy of the shared state.
If the store type is movable only, then the future can have only one continuation. Not really strange coming from Sean, future and packaged_task are copyables :) One more thing, there is not get no wait, but a get_try that return an optional<T>.
Intresting, so if a consumer needs wait must implement it on top of then. This is fine, but you need a way to unregister a 'then'. I don't know. The std::experimental::future::then is the TS hasn't way to unregister a continuation. Why do you think it is needed now? It is because the future is shared?
Ah, I forget, as futures are executed using schedulers it is possible to cancel them (or cancel the execution of the associated packaged task).
Scales better. the interface is certanly nicer and simpler, but why do you think it scales better?
Hum, in fact it is independent of the split. It is because the futures created by async don't block on destruction.
[...]
What are you missing ? [...]
The when_any continuation probably needs to be passed the all the futures instead of just the value that is ready, so that it can be called recursively.
I would consider when_any as having associated a variant of the Ts as value type. The continuation should then be an overload of the alternatives. The continuation passed to all the futures could be an internal one that is called just once and cancelled just before the first call. Could you elaborate the recursive part?
when_any also requires the ability to unregister a continuation from a future, otherwise if called 'in a loop' the number of continuations associated with futures will grow.
You are right that they will grow. I'm wondering if this is a real interface issue or a QOI. Needs this use case a special consideration even if it could decrease the performances of more usual cases?
Implementing this eficiently is not trivial. Agreed.
-- gpd
Thanks for sharing you view, Vicente
On 24 Jun 2015 8:08 am, "Vicente J. Botet Escriba"
Le 24/06/15 02:18, Giovanni Piero Deretta a écrit :
On Wed, Jun 24, 2015 at 12:09 AM, Vicente J. Botet Escriba
wrote:
which opens a lot of optimiation opportunities (for example if the future is non copyable non movable, no memory allocation is required).
Sean' future is copyable and movable. Do you mean if the stored value? Are thinking of the future<void>
specialization?
When no memory allocation will be needed?
The default future will allocate, but a non-movable non-copiable variant is conceivable that stores the shared state inline. You get a promise/packaged_task from it and pass it to the producer which need not to know.
His future library doesn't include a shared_future as the future
contains a
value that can be observed several times.
This is my biggest issue with Sean's design. Sharing the state among multiple consumes pretty much requires heavy weight synchronization.
Could you elaborate? It this different for std::shared_future?
I can chose not to use shared_future and use a plain future if I'm concerned about the shared count cost. Note that now I believe that this aspect of Sean design is implementable without cost if the future is never copied.
But see below.
The continuation takes the value and not the future. So a future can have several continuations.
What's the thread safety guarantees of future? 'As safe as int' or, for example, concurrent calls to .then are allowed? If the former, a copy could be a real deep copy (a new shared state is allocated and broadcasting is internally done via 'then'). That's not how Sean future is currently implemented though.
Of course, concurrent calls to .then must be allowed. I don't see when would you need the copy of the shared state.
If the store type is movable only, then the future can have only one continuation. Not really strange coming from Sean, future and packaged_task are
copyables
:) One more thing, there is not get no wait, but a get_try that return an optional<T>.
Intresting, so if a consumer needs wait must implement it on top of then. This is fine, but you need a way to unregister a 'then'.
I don't know. The std::experimental::future::then is the TS hasn't way to unregister a continuation. Why do you think it is needed now? It is because
Well you do not need to allow concurrent calls to then if you allow copying the future. Every thread can use its own copy of the future, although I see why Sean designed it this way: a future is a placeholder for the underlying type and pure reads are not racy for sane types. the future is shared?
Sean future doesn't have wait for example. If you want to implement timed waits on top of then you need a way to unregister a waiter. Similarly for wait_any. You can work around it of course. [...]
What are you missing ?
[...]
The when_any continuation probably needs to be passed the all the futures instead of just the value that is ready, so that it can be called recursively.
I would consider when_any as having associated a variant of the Ts as value type. The continuation should then be an overload of the alternatives. The continuation passed to all the futures could be an internal one that is called just once and cancelled just before the first call.
Could you elaborate the recursive part?
when_any also requires the ability to unregister a continuation from a future, otherwise if called 'in a loop' the number of continuations associated with futures will grow.
You are right that they will grow. I'm wondering if this is a real interface issue or a QOI.
Needs this use case a special consideration even if it could decrease the
Let say I have N futures. As soon as one them is ready I want to do something. I Call when_any with a continuation. After that I might want wait again for the remaining N-1 futures and maybe rearm the one that just got ready. If the continuation had access to all the futures then it would only need to call when_any again with itself as the continuation. This is equivalent to calling an hypothetical blocking wait_any in a loop. performances of more usual cases?
My use case is implementing a coroutine variant of wait_any efficiently on top of when_any. See: https://github.com/gpderetta/libtask/blob/master/tests/future_test.cpp (Look for wait_any)
On 24 Jun 2015 at 1:09, Vicente J. Botet Escriba wrote:
Sean Parent suggests (See [1] Better Code: Concurrency) to remove the promise and left the packaged_task serve as single provider of a future. Both are created at once using the package factory.
I was at his talk at C++ Now. The first thing which put me off his proposal was that he appears to be unaware of the highly optimised and very flexible de facto task concurrency system in C++ which has been around for years, and has just recently been standardised. It's called ASIO. I also think packaged_task is overkill for creating futures. There are some times you really do just want a promise and nothing more. There are even times you just want to create a future standalone, and for that we now have make_ready_future().
His future library doesn't include a shared_future as the future contains a value that can be observed several times.
If I remember rightly, internally his future has a shared_ptr, which really means he's proposing that future becomes a shared_future. I had doubts before until that part, but that killed his proposal for me. It's a non-starter as a practical design for my needs, but is still useful for ideas.
The continuation takes the value and not the future. So a future can have several continuations. If the store type is movable only, then the future can have only one continuation. Not really strange coming from Sean, future and packaged_task are copyables :) One more thing, there is not get no wait, but a get_try that return an optional<T>.
The ability to return an optional from potentially blocking functions I think is nice. Very Rust. I'll have the same in my upcoming lightweight future-promise - as you correctly observed, my monad<T> is also an optional<T> with the right template alias.
Scales better. IIUC the implementation allows to implement when_all on top of future/packaged_task. The trick is that that the packaged_task can store some context.
async has always an Scheduler as parameter. Scheculers are just Copyable Callables having this signature void(void()). There is no need for function add/spawn.
when_all has also a callable as argument.
I find the Sean's design clean and simple (See the code [2]). What do you think of it? Do you see any issues on it?
I think it's very easy to do a beautiful design that trades off performance for a beautiful design. I also think you can design a system which has poor low wait object performance but excellent high wait object performance, and vice versa. The problem to be solved is what balance between the two is best for most users of your design. In the future-promise I am currently building, I always ask this question when thinking about a potential feature or design: "Will this future-promise be lightweight enough to promise on every single SHA256 round in a hash?" That's about 400-500 cycles per SHA256 round, so I want future-promise construction, set_value and promise destruction to fit into a budget of about twenty CPU cycles. I don't know if I'll achieve that, but that's my design goal. I want future promise to become really lightweight. shared_future will remain heavy for those who need it.
* I don't understand why when_all continuation has the futures as parameters (I suspect that this is a type on the documentation). * I would rename get_try to try_get. This function could return optional
. What are you missing ? * catch_error/recover that is already planed. * when_any
I'd like to see any proposed C++ task system integrate well with ASIO. Tick that box for me and I am interested. Eliminate memory allocation and exception_ptr from the future-promise cycle and I am very interested indeed. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Le 24/06/15 11:13, Niall Douglas a écrit :
On 24 Jun 2015 at 1:09, Vicente J. Botet Escriba wrote:
Sean Parent suggests (See [1] Better Code: Concurrency) to remove the promise and left the packaged_task serve as single provider of a future. Both are created at once using the package factory. I was at his talk at C++ Now.
The first thing which put me off his proposal was that he appears to be unaware of the highly optimised and very flexible de facto task concurrency system in C++ which has been around for years, and has just recently been standardised. It's called ASIO. Why do you mix any discussion about futures with ASIO? I also think packaged_task is overkill for creating futures. There are some times you really do just want a promise and nothing more. You could have a packaged_task that just acts as a promise. The single difference is the interface. There are even times you just want to create a future standalone, and for that we now have make_ready_future(). Agree for make_ready_future/make_exceptional_future. This is not incompatible with Sean design.
His future library doesn't include a shared_future as the future contains a value that can be observed several times. If I remember rightly, internally his future has a shared_ptr, which really means he's proposing that future becomes a shared_future. std::future has always some kind of shared_ptr to the shared state. The share nature on shared_future is related to the fact that the value can be read several times. I had doubts before until that part, but that killed his proposal for me. It's a non-starter as a practical design for my needs, but is still useful for ideas. Why? because it uses heap memory?
Scales better. IIUC the implementation allows to implement when_all on top of future/packaged_task. The trick is that that the packaged_task can store some context.
async has always an Scheduler as parameter. Scheculers are just Copyable Callables having this signature void(void()). There is no need for function add/spawn.
when_all has also a callable as argument.
I find the Sean's design clean and simple (See the code [2]). What do you think of it? Do you see any issues on it? I think it's very easy to do a beautiful design that trades off performance for a beautiful design. Where do you see there are performances issues that can not be fixed without changing the interface, that is that are not QOI. I also think you can design a system which has poor low wait object performance but excellent high wait object performance, and vice versa. The problem to be solved is what balance between the two is best for most users of your design. Where do you place the std::future design, the Sean's design and why not your design respecto the the previos clasification?
In the future-promise I am currently building, I always ask this question when thinking about a potential feature or design:
"Will this future-promise be lightweight enough to promise on every single SHA256 round in a hash?"
That's about 400-500 cycles per SHA256 round, so I want future-promise construction, set_value and promise destruction to fit into a budget of about twenty CPU cycles.
I don't know if I'll achieve that, but that's my design goal. I want future promise to become really lightweight. shared_future will remain heavy for those who need it. Do you mean shared_ptr? The best when talking about performance is to have some figures :)
* I don't understand why when_all continuation has the futures as parameters (I suspect that this is a type on the documentation). * I would rename get_try to try_get. This function could return optional
. What are you missing ? * catch_error/recover that is already planed. * when_any I'd like to see any proposed C++ task system integrate well with ASIO. Tick that box for me and I am interested. Eliminate memory allocation and exception_ptr from the future-promise cycle and I am very interested indeed.
Good luck with your non-allocating future design. Vicente
participants (3)
-
Giovanni Piero Deretta
-
Niall Douglas
-
Vicente J. Botet Escriba