TONGARI J
2015-10-07 14:34:28 UTC
Hi Gor,
I have some comments on the current design:
RAII
coroutine_handle is just like raw-pointer, it doesn't manages the resource
and it's even more dangerous and error-prone than manual memory management.
coroutine_handle should embrace RAII semantic, more precisely, the
unique-semantic. There should be at most one instance executing the same
coroutine at the same time.
When coroutine_handle is destructed, it should force the stack to be
unwound.
I'd suggest renaming coroutine_handle to coroutine. std::coroutine<> should
define a promise_type, so it can be used as the *Coroutine Return Type*:
std::coroutine<> coro(int i, int e)
{
for (; i != e; ++i)
{
await std::suspend_always{};
std::cout << i;
}
}
...// usageauto c = coro(1, 10);while (c)
{
c();
std::cout << ", ";
}
There's no need for a done() member function, when the coroutine is done,
it'll reset itself so operator bool will report false.
std::coroutine<Promise> has a static void destroy(Promise*) function, which
destroys the coroutine-frame from the Promise pointer. Unlike the original
proposal, in which destroy is a non-static member function and will unwind
the stack, the new destroy is a static function and won't unwind the stack.
The Promise::get_return_object is also changed to accepts a param, i.e. get_return_object(coroutine<Promise>&
coro). The user can now decide when and how to run the coroutine on the
first time.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#case-study>Case Study
Consider the code below:
std::future<void> f()
{
Resource res(...);
await std::suspend_always{};
}
...// usagef().get();
Without RAII, you'll get memory & other resource leaks; with RAII, you'll
get broken_promise exception without any resource leak.
As an additional benefit, a coroutine can now safely cancel itself (i.e.
just suspend it), it also makes cascading cancellation possible without
exceptions.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#implementation>
Implementation
The idea is already implemented in CO2 <https://github.com/jamboree/co2>,
an emulation library based on earlier proposal.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#additional-optional-customization-points-for-promise>Additional
(optional) customization points for Promise
-
void cancel() This allows you specify the behavior of the coroutine when
it is cancelled (i.e. when cancellation_requested() returns true or
coroutine is reset).
-
bool resume() This is called before the coroutine is resumed, if it
returns false, the coroutine won't be resumed, instead, the coroutine
will be detached.
-
void suspend() This is called before the coroutine is gonna be
suspended, however, it won't be called for final_suspend.
Although theoretical there should be at most one instance executing the
same coroutine at the same time, but sometimes you need to resume more than
one concurrently, for example, to support "active-cancellation". The
resume() & suspend() customization points provide such possibility.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#bikeshedding>
Bikeshedding
- on_cancel
- on_resume
- on_suspend
<https://gist.github.com/jamboree/611b8c6551f853956f7d#implementation-1>
Implementation
Also implemented in CO2.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#restoration-of-cancellation_requested>Restoration
of cancellation_requested
I think cancellation_requested is still needed to implement
passive-cancellation.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#inspect-promisevalue_typereturn_type-check-on-return>Inspect
Promise::value_type(return_type), check on return
The return_type information could help the compiler to provide more robust
return behavior. It allows the compiler to check type compatibility before
calling Promise::return_value/void.
- return reference to local warning
// return type is a reference
std::future<int&> f()
{
int a = 9;
await std::experimental::suspend_never{};
return a; // warning wanted
}
- return local variable with the same type as return_type:
// return type and object have the same type
std::future<MoveOnly> f()
{
MoveOnly var;
await std::experimental::suspend_never{};
return var; // `var` should be treated as rvalue-ref
}
The Promise::return_value will get the param as rvalue-ref instead of
lvalue-ref.
- help in case that the Promise::return_value is generic that can't
deduce the type from braced-init:
std::future<Aggregate> f()
{
await std::experimental::suspend_never{};
return {1, 2, 3};
}
<https://gist.github.com/jamboree/611b8c6551f853956f7d#bikeshedding-1>
Bikeshedding
value_type/return_type/result_type
<https://gist.github.com/jamboree/611b8c6551f853956f7d#when-does-stack-unwinding-happen>When
does stack-unwinding happen?
It'd better happen before Promise::return_value/void. Consider a normal
function call:
int f()
{
LovalVar local;
doSomething();
return 0;
}
...// on useint i = f();// `local` should be destructed before we get the return value
We'd expect the stack-frame of f get unwound before it returns, the same
expectation applies to resumable functions as well:
Task<int> f()
{
LovalVar local;
await doSomething();
return 0;
}
...// on useint i = await f();
The implementation of Task may invoke its continuation on return_value/void
or final_suspend, depends on the author's choice. To get the consistent
behavior, stack-unwinding should happen before return_value/void.
I checked the current MSVC implementation, it's done before final_suspend,
and sometimes before return_value/void, which is inconsistent.
See the example <https://gist.github.com/jamboree/9bbe2435677295475f36>.
The result I got is:
[ret_value]
return_value()
~A()
final_suspend()
------
[ret_void]
return_void()
~A()
final_suspend()
------
[ret_void_implicit]
~A()
return_void()
final_suspend()
------
where A is a local variable of the resumable function.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#customization-points>Customization
points
These customization points should be made public (user-accessible):
- await_ready
- await_suspend
- await_resume
What I mean "public" is that the user can call the free functions, like
below:
using std::await_ready;if (await_ready(someAwaitable))
{
...
}
Better yet, the customization points could follow what Eric Niebler has
suggested: https://ericniebler.github.io/std/wg21/D4381.html
Also, operator await needs to be public as well, but I'd suggest renaming
it to get_awaiter, which is easier to write for the user, and also allows
the possibility of Eric's proposal.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#case-study-1>Case
Study
Suppose you want to make a wrapper for any *Awaiatable* that allows you to
wait without extracting the result, it could be implemented this way:
template<class Task>struct ready_awaiter
{
Task task;
bool await_ready()
{
return std::await_ready(task);
}
template<class F>
auto await_suspend(F&& f)
{
return std::await_suspend(task, std::forward<F>(f));
}
void await_resume() noexcept {}
};
template<class Task>inline ready_awaiter<Task> ready_impl(Task&& task)
{
return {std::forward<Task>(task)};
}
template<class Task>inline auto ready(Task&& task)
{
return ready_impl(std::get_awaiter(std::forward<Task>(task)));
}
...// usage
await ready(someAwaitable);
<https://gist.github.com/jamboree/611b8c6551f853956f7d#initialfinal_suspend---bool-or-awaiatable>
initial/final_suspend - bool or Awaiatable?
I don't see why initial/final_suspend needs to return *Awaiatable*.
If initial/final_suspend returns bool, it's just like returning an
*Awaiatable* that onlys defines bool await_suspend.
In the example of final_suspend you shown, it's exactly the same as the bool version.
What am I missing?
On the other hand, yield_value does need to return *Awaiatable*, I also
reported it here
<https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/zEMU_F-QcBo>
.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#syntax-to-specify-an-allocator>Syntax
to specify an allocator
A new clause can be used, for example:
template<class Allocator>
std::future<void> f(Allocator alloc) new(alloc)
{
await ...
}
The is irrelevant to how coroutine_traits *default* allocates, if the user
specifies new(alloc), the alloc will override the default allocation
strategy defined by coroutine_traits.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#auto-await-vs-resumableasync>auto
await v.s. resumable(async)
P0054R0 draws the idea "Automatically Awaited Awaitables", however, I think
we'd better not to interfere with the type system. The original sample:
auto MyProxy::operator=(int output)
{
struct Awaitable auto await { ... };
return Awaitable{...};
}
could be written this way:
await auto MyProxy::operator=(int output)
{
struct Awaitable { ... };
return Awaitable{...};
}
Note that the await prefix is not part of the type system here, it just
instructs the compiler to await on what it returns.
That said, the "auto await" idea is not flexible enough, consider the
special functions, e.g. constructors & destructors, which could also be
resumable in theory, there's no way for them to use "auto await".
A better solution is to consider something like the approach taken by
Resumable Expressions (P0114R0).
I had a draft describing the joint idea:
https://gist.github.com/jamboree/a2b3fe32eeb8c21e820c
The same example written in the draft will look like:
async auto MyProxy::operator=(int output)
{
struct Awaitable { ... };
return await Awaitable{...};
}
The most interesting part of the draft is "Context-based overloading",
which I haven't seen in other proposals, I'd like to get some feedback from
you.
Thanks.
I have some comments on the current design:
RAII
coroutine_handle is just like raw-pointer, it doesn't manages the resource
and it's even more dangerous and error-prone than manual memory management.
coroutine_handle should embrace RAII semantic, more precisely, the
unique-semantic. There should be at most one instance executing the same
coroutine at the same time.
When coroutine_handle is destructed, it should force the stack to be
unwound.
I'd suggest renaming coroutine_handle to coroutine. std::coroutine<> should
define a promise_type, so it can be used as the *Coroutine Return Type*:
std::coroutine<> coro(int i, int e)
{
for (; i != e; ++i)
{
await std::suspend_always{};
std::cout << i;
}
}
...// usageauto c = coro(1, 10);while (c)
{
c();
std::cout << ", ";
}
There's no need for a done() member function, when the coroutine is done,
it'll reset itself so operator bool will report false.
std::coroutine<Promise> has a static void destroy(Promise*) function, which
destroys the coroutine-frame from the Promise pointer. Unlike the original
proposal, in which destroy is a non-static member function and will unwind
the stack, the new destroy is a static function and won't unwind the stack.
The Promise::get_return_object is also changed to accepts a param, i.e. get_return_object(coroutine<Promise>&
coro). The user can now decide when and how to run the coroutine on the
first time.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#case-study>Case Study
Consider the code below:
std::future<void> f()
{
Resource res(...);
await std::suspend_always{};
}
...// usagef().get();
Without RAII, you'll get memory & other resource leaks; with RAII, you'll
get broken_promise exception without any resource leak.
As an additional benefit, a coroutine can now safely cancel itself (i.e.
just suspend it), it also makes cascading cancellation possible without
exceptions.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#implementation>
Implementation
The idea is already implemented in CO2 <https://github.com/jamboree/co2>,
an emulation library based on earlier proposal.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#additional-optional-customization-points-for-promise>Additional
(optional) customization points for Promise
-
void cancel() This allows you specify the behavior of the coroutine when
it is cancelled (i.e. when cancellation_requested() returns true or
coroutine is reset).
-
bool resume() This is called before the coroutine is resumed, if it
returns false, the coroutine won't be resumed, instead, the coroutine
will be detached.
-
void suspend() This is called before the coroutine is gonna be
suspended, however, it won't be called for final_suspend.
Although theoretical there should be at most one instance executing the
same coroutine at the same time, but sometimes you need to resume more than
one concurrently, for example, to support "active-cancellation". The
resume() & suspend() customization points provide such possibility.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#bikeshedding>
Bikeshedding
- on_cancel
- on_resume
- on_suspend
<https://gist.github.com/jamboree/611b8c6551f853956f7d#implementation-1>
Implementation
Also implemented in CO2.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#restoration-of-cancellation_requested>Restoration
of cancellation_requested
I think cancellation_requested is still needed to implement
passive-cancellation.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#inspect-promisevalue_typereturn_type-check-on-return>Inspect
Promise::value_type(return_type), check on return
The return_type information could help the compiler to provide more robust
return behavior. It allows the compiler to check type compatibility before
calling Promise::return_value/void.
- return reference to local warning
// return type is a reference
std::future<int&> f()
{
int a = 9;
await std::experimental::suspend_never{};
return a; // warning wanted
}
- return local variable with the same type as return_type:
// return type and object have the same type
std::future<MoveOnly> f()
{
MoveOnly var;
await std::experimental::suspend_never{};
return var; // `var` should be treated as rvalue-ref
}
The Promise::return_value will get the param as rvalue-ref instead of
lvalue-ref.
- help in case that the Promise::return_value is generic that can't
deduce the type from braced-init:
std::future<Aggregate> f()
{
await std::experimental::suspend_never{};
return {1, 2, 3};
}
<https://gist.github.com/jamboree/611b8c6551f853956f7d#bikeshedding-1>
Bikeshedding
value_type/return_type/result_type
<https://gist.github.com/jamboree/611b8c6551f853956f7d#when-does-stack-unwinding-happen>When
does stack-unwinding happen?
It'd better happen before Promise::return_value/void. Consider a normal
function call:
int f()
{
LovalVar local;
doSomething();
return 0;
}
...// on useint i = f();// `local` should be destructed before we get the return value
We'd expect the stack-frame of f get unwound before it returns, the same
expectation applies to resumable functions as well:
Task<int> f()
{
LovalVar local;
await doSomething();
return 0;
}
...// on useint i = await f();
The implementation of Task may invoke its continuation on return_value/void
or final_suspend, depends on the author's choice. To get the consistent
behavior, stack-unwinding should happen before return_value/void.
I checked the current MSVC implementation, it's done before final_suspend,
and sometimes before return_value/void, which is inconsistent.
See the example <https://gist.github.com/jamboree/9bbe2435677295475f36>.
The result I got is:
[ret_value]
return_value()
~A()
final_suspend()
------
[ret_void]
return_void()
~A()
final_suspend()
------
[ret_void_implicit]
~A()
return_void()
final_suspend()
------
where A is a local variable of the resumable function.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#customization-points>Customization
points
These customization points should be made public (user-accessible):
- await_ready
- await_suspend
- await_resume
What I mean "public" is that the user can call the free functions, like
below:
using std::await_ready;if (await_ready(someAwaitable))
{
...
}
Better yet, the customization points could follow what Eric Niebler has
suggested: https://ericniebler.github.io/std/wg21/D4381.html
Also, operator await needs to be public as well, but I'd suggest renaming
it to get_awaiter, which is easier to write for the user, and also allows
the possibility of Eric's proposal.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#case-study-1>Case
Study
Suppose you want to make a wrapper for any *Awaiatable* that allows you to
wait without extracting the result, it could be implemented this way:
template<class Task>struct ready_awaiter
{
Task task;
bool await_ready()
{
return std::await_ready(task);
}
template<class F>
auto await_suspend(F&& f)
{
return std::await_suspend(task, std::forward<F>(f));
}
void await_resume() noexcept {}
};
template<class Task>inline ready_awaiter<Task> ready_impl(Task&& task)
{
return {std::forward<Task>(task)};
}
template<class Task>inline auto ready(Task&& task)
{
return ready_impl(std::get_awaiter(std::forward<Task>(task)));
}
...// usage
await ready(someAwaitable);
<https://gist.github.com/jamboree/611b8c6551f853956f7d#initialfinal_suspend---bool-or-awaiatable>
initial/final_suspend - bool or Awaiatable?
I don't see why initial/final_suspend needs to return *Awaiatable*.
If initial/final_suspend returns bool, it's just like returning an
*Awaiatable* that onlys defines bool await_suspend.
In the example of final_suspend you shown, it's exactly the same as the bool version.
What am I missing?
On the other hand, yield_value does need to return *Awaiatable*, I also
reported it here
<https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/zEMU_F-QcBo>
.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#syntax-to-specify-an-allocator>Syntax
to specify an allocator
A new clause can be used, for example:
template<class Allocator>
std::future<void> f(Allocator alloc) new(alloc)
{
await ...
}
The is irrelevant to how coroutine_traits *default* allocates, if the user
specifies new(alloc), the alloc will override the default allocation
strategy defined by coroutine_traits.
<https://gist.github.com/jamboree/611b8c6551f853956f7d#auto-await-vs-resumableasync>auto
await v.s. resumable(async)
P0054R0 draws the idea "Automatically Awaited Awaitables", however, I think
we'd better not to interfere with the type system. The original sample:
auto MyProxy::operator=(int output)
{
struct Awaitable auto await { ... };
return Awaitable{...};
}
could be written this way:
await auto MyProxy::operator=(int output)
{
struct Awaitable { ... };
return Awaitable{...};
}
Note that the await prefix is not part of the type system here, it just
instructs the compiler to await on what it returns.
That said, the "auto await" idea is not flexible enough, consider the
special functions, e.g. constructors & destructors, which could also be
resumable in theory, there's no way for them to use "auto await".
A better solution is to consider something like the approach taken by
Resumable Expressions (P0114R0).
I had a draft describing the joint idea:
https://gist.github.com/jamboree/a2b3fe32eeb8c21e820c
The same example written in the draft will look like:
async auto MyProxy::operator=(int output)
{
struct Awaitable { ... };
return await Awaitable{...};
}
The most interesting part of the draft is "Context-based overloading",
which I haven't seen in other proposals, I'd like to get some feedback from
you.
Thanks.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.