问题
I've been playing around with C++20 coroutines and trying to move some of my codebase over to use them. I've run into an issue, though, as it doesn't seem that the new coroutines can be copied. The generator
objects have deleted copy-constructors and copy-assignment operators, and nothing I've looked into has seemed to have a way.
Can this be done?
For reference, I have written a little test program with a failing attempt at copying C++20 coroutines as well as a successful attempt to do the same thing with boost::asio::coroutine
. This is using Visual Studio 2019 version 16.3.7
#include <sdkddkver.h>
#include <string>
#include <algorithm>
#include <iterator>
#include <experimental\resumable>
#include <experimental\generator>
#include <cassert>
#include <boost\asio\yield.hpp>
namespace std_coroutines {
auto letters() {
for (auto c = 'a'; ; ++c)
co_yield c;
}
void run() {
auto gen = letters();
std::string s1, s2;
std::copy_n(gen.begin(), 3, std::back_inserter(s1)); // append "abc" to s1
//auto gen_copy = gen; // doesn't compile
std::copy_n(gen.begin(), 3, std::back_inserter(s1)); // append "def" to s1
//std::copy_n(gen_copy.begin(), 3, std::back_inserter(s2)); // append "def" to s2
assert(s1 == "abcdef");
assert(s2 == "def"); // fails
}
};// namespace std_coroutines
namespace boost_asio_coroutines {
struct letters : boost::asio::coroutine {
char c = 'a';
char operator()() {
reenter(this) for (;; ++c)
{
yield return c;
}
}
};
void run() {
auto gen = letters();
std::string s1, s2;
std::generate_n(std::back_inserter(s1), 3, std::ref(gen)); // append "abc" to s1
auto gen_copy = gen;
std::generate_n(std::back_inserter(s1), 3, std::ref(gen)); // append "def" to s1
std::generate_n(std::back_inserter(s2), 3, std::ref(gen_copy)); // append "def" to s2
assert(s1 == "abcdef");
assert(s2 == "def");
}
} // namespace boost_asio_coroutines
int main() {
boost_asio_coroutines::run();
std_coroutines::run();
}
回答1:
The TS doesn't explicitly prevent copies. As you mentioned, std::experimental::generator
promise object has deleted copy operations. I think that initial implementations are being conservative about copies because there is a lot to consider.
Coroutines manage a handle to the coroutine's activation context, formally called the coroutine state by N4775. Whether this state is on the heap or the stack (for optimization reasons), is implementation-defined. The format of the coroutine-storage itself is also implementation-defined.
A shallow copy could be accomplished by the implementation so long as ownership semantics of the coroutine handle were established along the lines of shared_ptr
and weak_ptr
(the analogy falls apart a bit because only one coroutine is the actual owner of the state, while all others are observers).
If you're asking about deep copies, where you end up with two separate generators that do not influence each other, I suppose it would be possible, too, with all the implications.
Some implications I can think of:
- deep-copying a function's local variables is arbitrarily expensive, and this extra allocation overhead may be an unexpected surprise for the user.
- a copied-coroutine handle would always* need to live on the heap, meaning that you may end up in a situation where the "original" coroutine has great performance, but the copied one has poor performance. Accidental copies may silently kill optimization.
*Storage for coroutines is obtained by calling a global non-array new
function. You could theoretically overload this function to match your coroutine, but since the storage is implementation-defined, you'd have to know your platform. Still, this would allow you to theoretically utilize a global arena allocator that has reserved a chunk of the stack, for example
回答2:
When I speak of "copying a coroutine", what I mean is, essentially, performing a copy operation on the "future" object returned by a coroutine function that results in something a user might consider a "copy" of that future.
Shallow copying of a coroutine (the copied future references the same coroutine_handle
and promise object) may have some small utility in certain cases. For non-generator scenarios, you're effectively creating the equivalent of a std::shared_future
: there are multiple locations from which the promised value(s) can be extracted.
I'm not really sure how useful that is for generator scenarios. It would become much more difficult to reason about what the generator is doing and where it is along its execution. It would also basically kill any hope of allocation elision, since you would have multiple references to the promise/handle.
Deep copying of a coroutine is all but impossible. To deep copy a coroutine, you would need to copy the stack of such a coroutine. Even if we assume that the coroutine is guaranteed not to be executing during the copy operation, this isn't really workable. Why?
Because the stack may contain objects which are non-copyable. And coroutines are not required to be inline, so the compiler which has to compile the copy operation doesn't necessarily have access to the source code of the coroutine itself. Thus, it can't determine if the coroutine stack is copyable.
Now, it would be at least hypothetically possible to have some kind of runtime test to see if copying is a possible operation for that coroutine. But that is its own can of worms.
But any kind of deep copying would require changes to the coroutine machinery; shallow copying is something you could hypothetically implement in your own future type.
The reason your code has deep-copy semantics is that it's not really using the coroutine promise/future machinery. Neither the promise nor the executing coroutine are holding a value; your coroutine object is. So copying your coroutine object creates a copy of that value. Actual C++20 coroutines don't work that way.
来源:https://stackoverflow.com/questions/58638238/can-c20-coroutines-be-copied