Home | Libraries | People | FAQ | More |
The initial version of Boost.Coroutine reference counted the
coroutine class template. Also the coroutine::self
type
was an alias for the coroutine
class itself. The rationale was
that, when used in a symmetric coroutine design it would be easy for a
coroutine to pass a copy of itself to other coroutines without needing
any explicit memory management. When all other coroutines dropped all
references to a specific coroutine that was deleted. Unfortunately
this same desirable behavior could backfire horribly if a cycle of
coroutines where to be formed.
In the end reference counting behavior was removed from the coroutine
interface andcoroutine where made movable. The same change lead to
the creation of coroutine::self to segregate coroutine
body specific operations (like yield and yield_to). Internally
reference counting is still used to manage coroutine lifetime when
future are used. While this can still lead to cycles if a coroutine
stores the result of coro::make_callback()
in a local, this is
explicitly prohibited in the interface, and should look suspiciously
wrong in code.
Futures were made movable for similar reasons.
current_coroutine
Boost.Coroutine provides no way to retrieve a reference to the current
coroutine. This is first of all for reasons of type safety. Every
coroutine is typed on its signature, so would be current pointer. The
user of an hypothetical current_coroutine
would need to pass to this
function, as a template parameter, the signature of the coroutine that
should be extracted. This signature would be checked at run time with
the signature of the current coroutine. Given that current_coroutine
would be most useful in generic code, the signature would need to be
passed down to the to the function that need to access the current
coroutine. At this point there is little benefit on passing only the
signature instead of a reference to self
.
The second reason is that current_coroutine
is a global object in
disguise. Global objects lead often to non scalable code. During the
development of the library and during testing, is has always been
possible to do away with the need for such a global by exploring other
solutions. The Win32 fiber API
provides a symmetric coroutine
interface with such a global object. Coding around the interface
mismatch between the Boost.Coroutine API
and the fiber API
has
been difficult and a potential source of
inefficiency.
The last reason for not providing a current_coroutine
is that this
could be used to yield
. Suppose a coroutine that is manipulating
some shared data calls a seemingly innocuous function; this coroutine
might invoke current_coroutine().yield()
, thus relinquishing control
and leaving the shared state with an invalid invariant. Functions that
may cause a coroutine to yield should documented as such. With the
current interface, these functions need a reference to self
. Passing
such a reference is a strong hint that the function might yield.
The main context is the flow of control outside of any coroutine
body. It is the flow of control started by main()
or from the
startup of any threads. Some coroutine APIs treat the main
context itself as a coroutine. Such libraries usually provide
symmetric coroutines, and treating main()
as a coroutine is the only
way to return to the main context. Boost.Coroutine is mostly designed around
asymmetric coroutines, so a normal yield()
can be used to return to
the main context.
Treating main()
as a coroutine also opens many problems:
coroutine<void()>
, but this seems too arbitrary.
self
. A default
constructed self
is not a solution, because it breaks the invariant
that two self
objects always refer to two different objects. We have
already reject the solution of a current_coroutine()
.
init_main()
function. This cannot be done statically because it must
be done for each new thread. Leaving the responsibility to the users of
the library opens the problem of two libraries trying both to
initialize the current context.
It has been argued [Moura04] that asymmetric coroutines are the best coroutine abstraction, because are simpler and safer than symmetric coroutines, while having the same expressiveness. We agree with that and the library has been developed around an asymmetric design.
During development was apparent that symmetric functionality could be
added without compromising the API
, thus yield_to
was
implemented. While yield_to
shouldn't be abused, it might
simplify some scenarios. It might also be a performance optimization.
"Premature optimization is the root of all evil" --
C. A. R. Hoare. While working on the Boost.Asio integration, the author thought that the only way to get good dispatching performance would be to use a specialized scheduler that used yield_to to go
from coroutine to coroutine. In the end the performance of
invoke/yield + invoke/yield was so close to that of
invoke/yield_to/yield that the need of a separate scheduler
disappeared greatly simplifying performance as an asio::io_service
works perfectly as a scheduler. |
Most cooperative threading libraries (for example the Pth library) deal with blocking behavior by wrapping asynchronous call behind a synchronous interface in the belief that asynchronous calls are a source of problems. Your author instead believes that are not the asynchronous calls themselves that complicate code, but the need to divide related code into multiple independent callback functions. Thus Boost.Coroutine doesn't try to hide the powerful Boost.Asio asynchronous interface behind a synchronous one, but simply helps dealing with the control inversion complication caused by the proliferation of small callbacks.
In fact _coroutines_ are not meant to be the silver bullet. Sometimes separated callbacks (maybe even defined in line with the help of Boost.Bind or Boost.Lambda) might be the right solution. One can even mix both styles together and use the best tool for each specific job.
It follows from the previous point that Boost.Coroutine is not a generalized
asynchronous framework. Do not confuse wait
as a general purpose
demultiplexer. The ability to wait for multiple futures is provided to
simplify some scenarios, like performing an operation while waiting
for a timer to expire, or reading and writing from two different
pipes. A coroutine that routinely waits for more that two or three futures,
should probably refactored in multiple coroutines.
Copyright © 2006 Giovanni P. Deretta |