![]() |
Home | Libraries | People | FAQ | More |
The type of coroutines we have described so far is usually referred as asymmetric. The asymmetry is due to the fact that the caller/callee relation between a coroutine's context and caller's context is fixed. The control flow must necessarily go from the caller context to the coroutine context and back to the caller. In this model a coroutine A can obviously call coroutine B, but A becomes the caller. B cannot directly yield to the caller of A but must relinquish control to A by yielding. For example, this control flow is not possible for example:
A yield to B yield to C yield to A yield to B ... etc
![]() |
Control flow with symmetric coroutines instead is not stack-like. A coroutine can always yield freely to any other coroutine and is not restricted to return to its caller. The previous control flow is possible.
While asymmetric coroutines are the main abstraction provided by Boost.Coroutine, a symmetric coroutine facility is also provided.
The coroutine class template has a yield_to()
member
function that stops the current coroutine and yields
control to a different coroutine. It works exactly like
yield()
, except that the control is not returned to the caller
but is given to another coroutine, specified as the first
argument. The target coroutine can be any other coroutine as long as
one of these conditions is true:
From the above conditions it follows that a coroutine can yield to
itself (in this case yield_to
is as if had returned immediately).
If coroutine A yields to coroutine B, the caller of A becomes the caller of B. If B ever does a normal yield, the control is given back to the caller of A.
![]() coroutine::operator() , while the
second an invocation of coroutine::yield_to . A coroutine
that yields to a second does not call the second one. |
As Boost.Coroutine strives for type safety, it requires that the return type of the yielded coroutine be the same of the yielder. For example, given these three coroutines:
typedef coroutine<int(char*, float&)> coroutine1_type;
typedef coroutine<int(int, float)> coroutine2_type;
typedef coroutine<void *(const& char)> coroutine3_type;
coroutine1_type coroutine1(coroutine1_body);
coroutine2_type coroutine2(coroutine2_body);
coroutine3_type coroutine2(coroutine3_body);
This code is legal:
//in coroutine1_body:
self.yield_to(coroutine2, 10, 0.0);
This is not:
//in coroutine1_body
self.yield_to(coroutine3, 'a'); // return type mismatch!
There is no restriction on the argument type.
![]() yield_to() is like goto on steroid. While it
can be extremely expressive and powerful, if it used without care and
discipline can easily lead to spaghetti code. |
We have explored the consumer and producer driven versions of this path before. In this third installment we will implement the pattern with the producer and the consumer as peer symmetric coroutines. The implementation is straight forward. These the our consumer and the producer bodies:
void producer_body(producer_type::self& self,
std::string base,
consumer_type& consumer) {
std::sort(base.begin(), base.end());
do {
self.yield_to(consumer, base);
} while (std::next_permutation(base.begin(), base.end()));
}
void consumer_body(consumer_type::self& self,
const std::string& value,
producer_type& producer) {
std::cout << value << "\n";
while(true) {
std::cout << self.yield_to(producer)<< "\n";
}
}
Creating the coroutines themselves is done as usual:
producer_type producer;
consumer_type consumer;
producer = producer_type
(boost::bind
(producer_body,
_1,
"hello",
boost::ref(consumer)));
consumer = consumer_type
(boost::bind
(consumer_body,
_1,
_2,
boost::ref(producer)));
Note how we default construct both producer
and consumer
before
actually initializing them with the bodies: we need to pass to
each coroutine a reference to the other. Also note the use of
boost::ref
to prevent boost::bind
to try to copy our non copyable
coroutines.
We can start the machinery indifferently from the producer:
...
producer();
...
Or from the consumer:
...
consumer (std::string());
...
We need to provide an argument to the consumer because it expect to
receive a value the first time it is called. For simplicity we
provided an empty string. A better solution would have had the
consumer accept boost::optional<const std::string&>
.
It can be demonstrated [Moura04] that both symmetric and asymmetric coroutines have the same expressive power, that is each type can be expressed in term of the other. We now will show how.
An asymmetric coroutine call can be implemented with yield_to
by
yielding to the called coroutine and passing as a parameter a
reference to the caller coroutine. yield
can be implemented
with a yield_to
the caller. This transformation is extremely
simple and intuitive. In fact the lowest levels of the
library only deal with a special swap_context
function. swap_context
works as an
argument-less yield_to
. Both yield
and yield_to
are
implemented in terms of this function.
Implementing yield_to
with only asymmetric coroutines is a bit
more involved, but still straight forward. In fact we already did
implement a form of it in our scheduler
example. A dispatch loop invokes the first coroutine. This
coroutine then chooses the next coroutine to run by returning to the
dispatcher the address of the target coroutine. The dispatch loop then
execute that coroutine and so on.
In conclusion Boost.Coroutine could implement only one of the two models and not loose expressiveness. Given a choice we would implement asymmetric coroutines because they are simpler to understand, safer and have a broader application. We decided to provide both models for convenience.
Copyright © 2006 Giovanni P. Deretta |