Implementing coroutines with ucontext
by Malte Skarupke
Coroutines are a way of doing cooperative multithreading where each context has it’s own stack. And they can be incredibly useful for games. (I’ll use the word context as the coroutine equivalent of a thread) The basic idea of a coroutine is that you can yield from a context in the middle of a function, and then you will continue exactly at that point when the coroutine gets called the next time.
Imagine that you want something to happen in a sequence, for example you want to spawn an enemy, show a text for two seconds, then kill the enemy and show another text for two seconds. A trivial implementation would be this:
void sudden_exploding_unit(const Vector3 & pos) { std::unique_ptr<Entity> guy = spawn_entity(pos, "grunt.entity"); sleep(1.0f); show_text(pos, "oh no he is exploding!", 2.0f); sleep(0.5f); explode(*guy); sleep(1.0f); show_text(pos, "well that was underwhelming", 2.0f); }
Even though this code is simple, you would probably have a large amount of pain to implement this in a game engine, because a game can never block, and this code is sleeping all over the place. You’d probably end up writing some kind of state machine (maybe using a kismet-like scripting system) or if you’re a terrible person you’d use asynchronous callbacks. State machines can be a fine solution, but they introduce way too much infrastructure for such a trivial piece of code.
Enter coroutines. If your engine has coroutines, you can write exactly the above code. With very little downsides. Because you could just sleep like this:
void sleep(float seconds) { while (seconds >= 0.0f) { yield(); // resume here next frame seconds -= global_dt; } }
With the assumption that your coroutine gets called every frame.
The power of this is that you keep your callstack and your instruction pointer. Dijkstra wrote an article in 1968 about why the goto statement is harmful: The problem is, that it makes it difficult to determine the current state of the program. And as the this article points out, you can replace the word “goto” in that article with the term “asynchronous callback” and it would still be relevant. Basically it comes down to this: If you put a breakpoint in your code, how much information will you have? Asynchronous callbacks and state machines are only mildly better than the goto statement in this regard, in that it can be difficult to determine where the current state comes from.
Coroutines have less of a problem there because you are using the right tools to keep the information on how far along you are in the process: the instruction pointer and the callstack. The downside is that one function could have had many different entry points in it’s lifetime, but I don’t consider that a big problem.
Here’s a video where Nat Goodspeed of Linden Lab explains how they untangled their streaming code using coroutines. The issue with streaming code is that you typically have a update function which gets called every frame that looks like this:
void updated_request(stream_request & request) { switch(request.status) { case NOT_STARTED: send_request(request); break; case WAITING_FOR_SERVER: if (request.start_time - global_currenttime > timeout) { cancel_request(request); } break; case RECEIVED: load_data(request); break; case LOADING: break; case LOADED: instantiate_object(request); break; } }
Which still looks simple enough, but this function will grow forever. Sending a request to the server probably requires authentication. And you’ll have to decrypt and decompress the data. None of which must happen in the main thread. Of course you also have to be able to cancel your request at any moment.
Nat Goodspeed says that this function grew to 1800 lines in their codebase. Which is not too unlikely. And which is an unmaintainable mess, because it is much less clear than a 1800 line function which gets executed in order. At 16:23 in the video he explains a situation where they realized that a call inside of a loop was actually blocking. They were able to transition to a non-blocking equivalent by introducing a new state. Except since they were inside a loop they had to store the loop control variable somewhere and introduce even more complexity.
With coroutines you can rewrite the above function like this:
void handle_request(stream_request & request) { if (send_request(request, timeout) == TIMED_OUT) { return; } load_data(request); instantiate_object(request); }
And you just say that it is the responsibility of send_request to yield until it is finished or times out, which is a reasonable requirement.
Linden Lab uses boost::coroutine which offers a coroutine implementation for C++. The issue is that it is aiming to be a typical boost library.
For example here are all the includes you get merely for including “boost/coroutine/coroutine.hpp”:
include_only.o:include_only.cpp
boost/coroutine/coroutine.hpp
boost/coroutine/detail/default_context_impl.hpp
/usr/include/boost/config.hpp
/usr/include/boost/config/user.hpp
/usr/include/boost/config/select_compiler_config.hpp
/usr/include/boost/config/compiler/gcc.hpp
/usr/include/boost/config/select_stdlib_config.hpp
/usr/include/c++/4.7/cstddef
/usr/include/c++/4.7/x86_64-linux-gnu/bits/c++config.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/os_defines.h
/usr/include/features.h
/usr/include/x86_64-linux-gnu/bits/predefs.h
/usr/include/x86_64-linux-gnu/sys/cdefs.h
/usr/include/x86_64-linux-gnu/bits/wordsize.h
/usr/include/x86_64-linux-gnu/gnu/stubs.h
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/cpu_defines.h
/usr/lib/gcc/x86_64-linux-gnu/4.7/include/stddef.h
/usr/include/boost/config/no_tr1/utility.hpp
/usr/include/c++/4.7/utility
/usr/include/c++/4.7/bits/stl_relops.h
/usr/include/c++/4.7/bits/stl_pair.h
/usr/include/c++/4.7/bits/move.h
/usr/include/c++/4.7/bits/concept_check.h
/usr/include/c++/4.7/type_traits
/usr/include/c++/4.7/initializer_list
/usr/include/boost/config/stdlib/libstdcpp3.hpp
/usr/include/unistd.h
/usr/include/x86_64-linux-gnu/bits/posix_opt.h
/usr/include/x86_64-linux-gnu/bits/environments.h
/usr/include/x86_64-linux-gnu/bits/types.h
/usr/include/x86_64-linux-gnu/bits/typesizes.h
/usr/include/x86_64-linux-gnu/bits/confname.h
/usr/include/getopt.h
/usr/include/boost/config/select_platform_config.hpp
/usr/include/boost/config/platform/linux.hpp
/usr/include/c++/4.7/cstdlib
/usr/include/stdlib.h
/usr/include/x86_64-linux-gnu/bits/waitflags.h
/usr/include/x86_64-linux-gnu/bits/waitstatus.h
/usr/include/endian.h
/usr/include/x86_64-linux-gnu/bits/endian.h
/usr/include/x86_64-linux-gnu/bits/byteswap.h
/usr/include/xlocale.h
/usr/include/x86_64-linux-gnu/sys/types.h
/usr/include/time.h
/usr/include/x86_64-linux-gnu/sys/select.h
/usr/include/x86_64-linux-gnu/bits/select.h
/usr/include/x86_64-linux-gnu/bits/sigset.h
/usr/include/x86_64-linux-gnu/bits/time.h
/usr/include/x86_64-linux-gnu/sys/sysmacros.h
/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h
/usr/include/alloca.h
/usr/include/boost/config/posix_features.hpp
/usr/include/boost/config/suffix.hpp
boost/coroutine/detail/context_linux.hpp
boost/coroutine/detail/context_posix.hpp
/usr/include/boost/assert.hpp
/usr/include/assert.h
/usr/include/c++/4.7/iostream
/usr/include/c++/4.7/ostream
/usr/include/c++/4.7/ios
/usr/include/c++/4.7/iosfwd
/usr/include/c++/4.7/bits/stringfwd.h
/usr/include/c++/4.7/bits/postypes.h
/usr/include/c++/4.7/cwchar
/usr/include/wchar.h
/usr/include/stdio.h
/usr/lib/gcc/x86_64-linux-gnu/4.7/include/stdarg.h
/usr/include/x86_64-linux-gnu/bits/wchar.h
/usr/include/c++/4.7/exception
/usr/include/c++/4.7/bits/atomic_lockfree_defines.h
/usr/include/c++/4.7/bits/exception_ptr.h
/usr/include/c++/4.7/bits/exception_defines.h
/usr/include/c++/4.7/bits/nested_exception.h
/usr/include/c++/4.7/bits/char_traits.h
/usr/include/c++/4.7/bits/stl_algobase.h
/usr/include/c++/4.7/bits/functexcept.h
/usr/include/c++/4.7/bits/cpp_type_traits.h
/usr/include/c++/4.7/ext/type_traits.h
/usr/include/c++/4.7/ext/numeric_traits.h
/usr/include/c++/4.7/bits/stl_iterator_base_types.h
/usr/include/c++/4.7/bits/stl_iterator_base_funcs.h
/usr/include/c++/4.7/bits/stl_iterator.h
/usr/include/c++/4.7/debug/debug.h
/usr/include/c++/4.7/cstdint
/usr/lib/gcc/x86_64-linux-gnu/4.7/include/stdint.h
/usr/include/stdint.h
/usr/include/c++/4.7/bits/localefwd.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/c++locale.h
/usr/include/c++/4.7/clocale
/usr/include/locale.h
/usr/include/x86_64-linux-gnu/bits/locale.h
/usr/include/c++/4.7/cctype
/usr/include/ctype.h
/usr/include/c++/4.7/bits/ios_base.h
/usr/include/c++/4.7/ext/atomicity.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/gthr.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/gthr-default.h
/usr/include/pthread.h
/usr/include/sched.h
/usr/include/x86_64-linux-gnu/bits/sched.h
/usr/include/x86_64-linux-gnu/bits/timex.h
/usr/include/x86_64-linux-gnu/bits/setjmp.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/atomic_word.h
/usr/include/c++/4.7/bits/locale_classes.h
/usr/include/c++/4.7/string
/usr/include/c++/4.7/bits/allocator.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/c++allocator.h
/usr/include/c++/4.7/ext/new_allocator.h
/usr/include/c++/4.7/new
/usr/include/c++/4.7/bits/ostream_insert.h
/usr/include/c++/4.7/bits/cxxabi_forced.h
/usr/include/c++/4.7/bits/stl_function.h
/usr/include/c++/4.7/backward/binders.h
/usr/include/c++/4.7/bits/range_access.h
/usr/include/c++/4.7/bits/basic_string.h
/usr/include/c++/4.7/ext/string_conversions.h
/usr/include/c++/4.7/cstdio
/usr/include/libio.h
/usr/include/_G_config.h
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h
/usr/include/c++/4.7/cerrno
/usr/include/errno.h
/usr/include/x86_64-linux-gnu/bits/errno.h
/usr/include/linux/errno.h
/usr/include/x86_64-linux-gnu/asm/errno.h
/usr/include/asm-generic/errno.h
/usr/include/asm-generic/errno-base.h
/usr/include/c++/4.7/bits/functional_hash.h
/usr/include/c++/4.7/bits/hash_bytes.h
/usr/include/c++/4.7/bits/basic_string.tcc
/usr/include/c++/4.7/bits/locale_classes.tcc
/usr/include/c++/4.7/streambuf
/usr/include/c++/4.7/bits/streambuf.tcc
/usr/include/c++/4.7/bits/basic_ios.h
/usr/include/c++/4.7/bits/locale_facets.h
/usr/include/c++/4.7/cwctype
/usr/include/wctype.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/ctype_base.h
/usr/include/c++/4.7/bits/streambuf_iterator.h
/usr/include/c++/4.7/x86_64-linux-gnu/bits/ctype_inline.h
/usr/include/c++/4.7/bits/locale_facets.tcc
/usr/include/c++/4.7/bits/basic_ios.tcc
/usr/include/c++/4.7/bits/ostream.tcc
/usr/include/c++/4.7/istream
/usr/include/c++/4.7/bits/istream.tcc
/usr/include/boost/current_function.hpp
/usr/include/ucontext.h
/usr/include/x86_64-linux-gnu/sys/ucontext.h
/usr/include/signal.h
/usr/include/x86_64-linux-gnu/bits/signum.h
/usr/include/x86_64-linux-gnu/bits/siginfo.h
/usr/include/x86_64-linux-gnu/bits/sigaction.h
/usr/include/x86_64-linux-gnu/bits/sigcontext.h
/usr/include/x86_64-linux-gnu/bits/sigstack.h
/usr/include/x86_64-linux-gnu/bits/sigthread.h
/usr/include/boost/noncopyable.hpp
boost/coroutine/exception.hpp
/usr/include/c++/4.7/typeinfo
boost/coroutine/detail/posix_utility.hpp
/usr/include/c++/4.7/cstring
/usr/include/string.h
/usr/include/boost/type_traits.hpp
/usr/include/boost/type_traits/add_const.hpp
/usr/include/boost/type_traits/detail/type_trait_def.hpp
/usr/include/boost/type_traits/detail/template_arity_spec.hpp
/usr/include/boost/mpl/int.hpp
/usr/include/boost/mpl/int_fwd.hpp
/usr/include/boost/mpl/aux_/adl_barrier.hpp
/usr/include/boost/mpl/aux_/config/adl.hpp
/usr/include/boost/mpl/aux_/config/msvc.hpp
/usr/include/boost/mpl/aux_/config/intel.hpp
/usr/include/boost/mpl/aux_/config/gcc.hpp
/usr/include/boost/mpl/aux_/config/workaround.hpp
/usr/include/boost/detail/workaround.hpp
/usr/include/boost/mpl/aux_/nttp_decl.hpp
/usr/include/boost/mpl/aux_/config/nttp.hpp
/usr/include/boost/mpl/aux_/integral_wrapper.hpp
/usr/include/boost/mpl/integral_c_tag.hpp
/usr/include/boost/mpl/aux_/config/static_constant.hpp
/usr/include/boost/mpl/aux_/static_cast.hpp
/usr/include/boost/preprocessor/cat.hpp
/usr/include/boost/preprocessor/config/config.hpp
/usr/include/boost/mpl/aux_/template_arity_fwd.hpp
/usr/include/boost/mpl/aux_/preprocessor/params.hpp
/usr/include/boost/mpl/aux_/config/preprocessor.hpp
/usr/include/boost/preprocessor/comma_if.hpp
/usr/include/boost/preprocessor/punctuation/comma_if.hpp
/usr/include/boost/preprocessor/control/if.hpp
/usr/include/boost/preprocessor/control/iif.hpp
/usr/include/boost/preprocessor/logical/bool.hpp
/usr/include/boost/preprocessor/facilities/empty.hpp
/usr/include/boost/preprocessor/punctuation/comma.hpp
/usr/include/boost/preprocessor/repeat.hpp
/usr/include/boost/preprocessor/repetition/repeat.hpp
/usr/include/boost/preprocessor/debug/error.hpp
/usr/include/boost/preprocessor/detail/auto_rec.hpp
/usr/include/boost/preprocessor/tuple/eat.hpp
/usr/include/boost/preprocessor/inc.hpp
/usr/include/boost/preprocessor/arithmetic/inc.hpp
/usr/include/boost/mpl/aux_/config/lambda.hpp
/usr/include/boost/mpl/aux_/config/ttp.hpp
/usr/include/boost/mpl/aux_/config/ctps.hpp
/usr/include/boost/mpl/aux_/config/overload_resolution.hpp
/usr/include/boost/mpl/aux_/lambda_support.hpp
/usr/include/boost/type_traits/detail/type_trait_undef.hpp
/usr/include/boost/type_traits/add_cv.hpp
/usr/include/boost/type_traits/add_lvalue_reference.hpp
/usr/include/boost/type_traits/add_reference.hpp
/usr/include/boost/type_traits/is_reference.hpp
/usr/include/boost/type_traits/config.hpp
/usr/include/boost/type_traits/is_lvalue_reference.hpp
/usr/include/boost/type_traits/detail/bool_trait_def.hpp
/usr/include/boost/type_traits/integral_constant.hpp
/usr/include/boost/mpl/bool.hpp
/usr/include/boost/mpl/bool_fwd.hpp
/usr/include/boost/mpl/integral_c.hpp
/usr/include/boost/mpl/integral_c_fwd.hpp
/usr/include/boost/type_traits/detail/bool_trait_undef.hpp
/usr/include/boost/type_traits/is_rvalue_reference.hpp
/usr/include/boost/type_traits/ice.hpp
/usr/include/boost/type_traits/detail/yes_no_type.hpp
/usr/include/boost/type_traits/detail/ice_or.hpp
/usr/include/boost/type_traits/detail/ice_and.hpp
/usr/include/boost/type_traits/detail/ice_not.hpp
/usr/include/boost/type_traits/detail/ice_eq.hpp
/usr/include/boost/type_traits/add_pointer.hpp
/usr/include/boost/type_traits/remove_reference.hpp
/usr/include/boost/type_traits/broken_compiler_spec.hpp
/usr/include/boost/type_traits/add_rvalue_reference.hpp
/usr/include/boost/type_traits/is_void.hpp
/usr/include/boost/type_traits/add_volatile.hpp
/usr/include/boost/type_traits/aligned_storage.hpp
/usr/include/boost/aligned_storage.hpp
/usr/include/boost/type_traits/alignment_of.hpp
/usr/include/boost/type_traits/intrinsics.hpp
/usr/include/boost/type_traits/is_same.hpp
/usr/include/boost/type_traits/is_volatile.hpp
/usr/include/boost/type_traits/detail/cv_traits_impl.hpp
/usr/include/boost/type_traits/detail/size_t_trait_def.hpp
/usr/include/boost/mpl/size_t.hpp
/usr/include/boost/mpl/size_t_fwd.hpp
/usr/include/boost/type_traits/detail/size_t_trait_undef.hpp
/usr/include/boost/type_traits/type_with_alignment.hpp
/usr/include/boost/mpl/if.hpp
/usr/include/boost/mpl/aux_/value_wknd.hpp
/usr/include/boost/mpl/aux_/config/integral.hpp
/usr/include/boost/mpl/aux_/config/eti.hpp
/usr/include/boost/mpl/aux_/na_spec.hpp
/usr/include/boost/mpl/lambda_fwd.hpp
/usr/include/boost/mpl/void_fwd.hpp
/usr/include/boost/mpl/aux_/na.hpp
/usr/include/boost/mpl/aux_/na_fwd.hpp
/usr/include/boost/mpl/aux_/lambda_arity_param.hpp
/usr/include/boost/mpl/aux_/arity.hpp
/usr/include/boost/mpl/aux_/config/dtp.hpp
/usr/include/boost/mpl/aux_/preprocessor/enum.hpp
/usr/include/boost/mpl/aux_/preprocessor/def_params_tail.hpp
/usr/include/boost/mpl/limits/arity.hpp
/usr/include/boost/preprocessor/logical/and.hpp
/usr/include/boost/preprocessor/logical/bitand.hpp
/usr/include/boost/preprocessor/identity.hpp
/usr/include/boost/preprocessor/facilities/identity.hpp
/usr/include/boost/preprocessor/empty.hpp
/usr/include/boost/preprocessor/arithmetic/add.hpp
/usr/include/boost/preprocessor/arithmetic/dec.hpp
/usr/include/boost/preprocessor/control/while.hpp
/usr/include/boost/preprocessor/list/fold_left.hpp
/usr/include/boost/preprocessor/list/detail/fold_left.hpp
/usr/include/boost/preprocessor/control/expr_iif.hpp
/usr/include/boost/preprocessor/list/adt.hpp
/usr/include/boost/preprocessor/detail/is_binary.hpp
/usr/include/boost/preprocessor/detail/check.hpp
/usr/include/boost/preprocessor/logical/compl.hpp
/usr/include/boost/preprocessor/list/fold_right.hpp
/usr/include/boost/preprocessor/list/detail/fold_right.hpp
/usr/include/boost/preprocessor/list/reverse.hpp
/usr/include/boost/preprocessor/control/detail/while.hpp
/usr/include/boost/preprocessor/tuple/elem.hpp
/usr/include/boost/preprocessor/facilities/overload.hpp
/usr/include/boost/preprocessor/variadic/size.hpp
/usr/include/boost/preprocessor/tuple/rem.hpp
/usr/include/boost/preprocessor/variadic/elem.hpp
/usr/include/boost/preprocessor/arithmetic/sub.hpp
/usr/include/boost/preprocessor/list/for_each_i.hpp
/usr/include/boost/preprocessor/repetition/for.hpp
/usr/include/boost/preprocessor/repetition/detail/for.hpp
/usr/include/boost/preprocessor/tuple/to_list.hpp
/usr/include/boost/preprocessor/list/transform.hpp
/usr/include/boost/preprocessor/list/append.hpp
/usr/include/boost/type_traits/is_pod.hpp
/usr/include/boost/type_traits/is_scalar.hpp
/usr/include/boost/type_traits/is_arithmetic.hpp
/usr/include/boost/type_traits/is_integral.hpp
/usr/include/boost/type_traits/is_float.hpp
/usr/include/boost/type_traits/is_enum.hpp
/usr/include/boost/type_traits/is_pointer.hpp
/usr/include/boost/type_traits/is_member_pointer.hpp
/usr/include/boost/type_traits/is_member_function_pointer.hpp
/usr/include/boost/type_traits/detail/is_mem_fun_pointer_impl.hpp
/usr/include/boost/type_traits/remove_cv.hpp
/usr/include/boost/static_assert.hpp
/usr/include/boost/mpl/eval_if.hpp
/usr/include/boost/mpl/identity.hpp
/usr/include/boost/type_traits/common_type.hpp
/usr/include/boost/utility/declval.hpp
/usr/include/boost/type_traits/conditional.hpp
/usr/include/boost/type_traits/decay.hpp
/usr/include/boost/type_traits/is_array.hpp
/usr/include/boost/type_traits/is_function.hpp
/usr/include/boost/type_traits/detail/false_result.hpp
/usr/include/boost/type_traits/detail/is_function_ptr_helper.hpp
/usr/include/boost/type_traits/remove_bounds.hpp
/usr/include/boost/type_traits/extent.hpp
/usr/include/boost/type_traits/floating_point_promotion.hpp
/usr/include/boost/type_traits/function_traits.hpp
/usr/include/boost/type_traits/has_new_operator.hpp
/usr/include/boost/type_traits/has_nothrow_assign.hpp
/usr/include/boost/type_traits/has_trivial_assign.hpp
/usr/include/boost/type_traits/is_const.hpp
/usr/include/boost/type_traits/has_nothrow_constructor.hpp
/usr/include/boost/type_traits/has_trivial_constructor.hpp
/usr/include/boost/type_traits/has_nothrow_copy.hpp
/usr/include/boost/type_traits/has_trivial_copy.hpp
/usr/include/boost/type_traits/has_nothrow_destructor.hpp
/usr/include/boost/type_traits/has_trivial_destructor.hpp
/usr/include/boost/type_traits/has_operator.hpp
/usr/include/boost/type_traits/has_bit_and.hpp
/usr/include/boost/type_traits/detail/has_binary_operator.hpp
/usr/include/boost/type_traits/is_base_of.hpp
/usr/include/boost/type_traits/is_base_and_derived.hpp
/usr/include/boost/type_traits/is_class.hpp
/usr/include/boost/type_traits/is_convertible.hpp
/usr/include/boost/type_traits/is_abstract.hpp
/usr/include/boost/type_traits/is_fundamental.hpp
/usr/include/boost/type_traits/remove_pointer.hpp
/usr/include/boost/type_traits/has_bit_and_assign.hpp
/usr/include/boost/type_traits/has_bit_or.hpp
/usr/include/boost/type_traits/has_bit_or_assign.hpp
/usr/include/boost/type_traits/has_bit_xor.hpp
/usr/include/boost/type_traits/has_bit_xor_assign.hpp
/usr/include/boost/type_traits/has_complement.hpp
/usr/include/boost/type_traits/detail/has_prefix_operator.hpp
/usr/include/boost/type_traits/has_dereference.hpp
/usr/include/boost/type_traits/has_divides.hpp
/usr/include/boost/type_traits/has_divides_assign.hpp
/usr/include/boost/type_traits/has_equal_to.hpp
/usr/include/boost/type_traits/has_greater.hpp
/usr/include/boost/type_traits/has_greater_equal.hpp
/usr/include/boost/type_traits/has_left_shift.hpp
/usr/include/boost/type_traits/has_left_shift_assign.hpp
/usr/include/boost/type_traits/has_less.hpp
/usr/include/boost/type_traits/has_less_equal.hpp
/usr/include/boost/type_traits/has_logical_and.hpp
/usr/include/boost/type_traits/has_logical_not.hpp
/usr/include/boost/type_traits/has_logical_or.hpp
/usr/include/boost/type_traits/has_minus.hpp
/usr/include/boost/type_traits/has_minus_assign.hpp
/usr/include/boost/type_traits/has_modulus.hpp
/usr/include/boost/type_traits/has_modulus_assign.hpp
/usr/include/boost/type_traits/has_multiplies.hpp
/usr/include/boost/type_traits/has_multiplies_assign.hpp
/usr/include/boost/type_traits/has_negate.hpp
/usr/include/boost/type_traits/has_not_equal_to.hpp
/usr/include/boost/type_traits/has_plus.hpp
/usr/include/boost/type_traits/has_plus_assign.hpp
/usr/include/boost/type_traits/has_post_decrement.hpp
/usr/include/boost/type_traits/detail/has_postfix_operator.hpp
/usr/include/boost/type_traits/has_post_increment.hpp
/usr/include/boost/type_traits/has_pre_decrement.hpp
/usr/include/boost/type_traits/has_pre_increment.hpp
/usr/include/boost/type_traits/has_right_shift.hpp
/usr/include/boost/type_traits/has_right_shift_assign.hpp
/usr/include/boost/type_traits/has_unary_minus.hpp
/usr/include/boost/type_traits/has_unary_plus.hpp
/usr/include/boost/type_traits/has_virtual_destructor.hpp
/usr/include/boost/type_traits/is_complex.hpp
/usr/include/c++/4.7/complex
/usr/include/c++/4.7/cmath
/usr/include/math.h
/usr/include/x86_64-linux-gnu/bits/huge_val.h
/usr/include/x86_64-linux-gnu/bits/huge_valf.h
/usr/include/x86_64-linux-gnu/bits/huge_vall.h
/usr/include/x86_64-linux-gnu/bits/inf.h
/usr/include/x86_64-linux-gnu/bits/nan.h
/usr/include/x86_64-linux-gnu/bits/mathdef.h
/usr/include/x86_64-linux-gnu/bits/mathcalls.h
/usr/include/c++/4.7/sstream
/usr/include/c++/4.7/bits/sstream.tcc
/usr/include/boost/type_traits/is_compound.hpp
/usr/include/boost/type_traits/is_empty.hpp
/usr/include/boost/type_traits/is_floating_point.hpp
/usr/include/boost/type_traits/is_member_object_pointer.hpp
/usr/include/boost/type_traits/is_object.hpp
/usr/include/boost/type_traits/is_polymorphic.hpp
/usr/include/boost/type_traits/is_signed.hpp
/usr/include/boost/type_traits/is_stateless.hpp
/usr/include/boost/type_traits/is_union.hpp
/usr/include/boost/type_traits/is_unsigned.hpp
/usr/include/boost/type_traits/is_virtual_base_of.hpp
/usr/include/boost/mpl/and.hpp
/usr/include/boost/mpl/aux_/config/use_preprocessed.hpp
/usr/include/boost/mpl/aux_/nested_type_wknd.hpp
/usr/include/boost/mpl/aux_/include_preprocessed.hpp
/usr/include/boost/mpl/aux_/config/compiler.hpp
/usr/include/boost/preprocessor/stringize.hpp
/usr/include/boost/mpl/aux_/preprocessed/gcc/and.hpp
/usr/include/boost/mpl/not.hpp
/usr/include/boost/type_traits/make_unsigned.hpp
/usr/include/boost/type_traits/make_signed.hpp
/usr/include/boost/type_traits/rank.hpp
/usr/include/boost/type_traits/remove_extent.hpp
/usr/include/boost/type_traits/remove_all_extents.hpp
/usr/include/boost/type_traits/remove_const.hpp
/usr/include/boost/type_traits/remove_volatile.hpp
/usr/include/boost/type_traits/integral_promotion.hpp
/usr/include/boost/type_traits/promote.hpp
/usr/include/x86_64-linux-gnu/sys/mman.h
/usr/include/x86_64-linux-gnu/bits/mman.h
boost/coroutine/detail/swap_context.hpp
/usr/include/boost/preprocessor/repetition.hpp
/usr/include/boost/preprocessor/repetition/deduce_r.hpp
/usr/include/boost/preprocessor/repetition/deduce_z.hpp
/usr/include/boost/preprocessor/repetition/enum.hpp
/usr/include/boost/preprocessor/repetition/enum_binary_params.hpp
/usr/include/boost/preprocessor/repetition/enum_params.hpp
/usr/include/boost/preprocessor/repetition/enum_params_with_a_default.hpp
/usr/include/boost/preprocessor/facilities/intercept.hpp
/usr/include/boost/preprocessor/repetition/enum_params_with_defaults.hpp
/usr/include/boost/preprocessor/repetition/enum_shifted.hpp
/usr/include/boost/preprocessor/repetition/enum_shifted_binary_params.hpp
/usr/include/boost/preprocessor/repetition/enum_shifted_params.hpp
/usr/include/boost/preprocessor/repetition/enum_trailing.hpp
/usr/include/boost/preprocessor/repetition/enum_trailing_binary_params.hpp
/usr/include/boost/preprocessor/repetition/enum_trailing_params.hpp
/usr/include/boost/preprocessor/repetition/repeat_from_to.hpp
/usr/include/boost/tuple/tuple.hpp
/usr/include/boost/ref.hpp
/usr/include/boost/utility/addressof.hpp
/usr/include/boost/tuple/detail/tuple_basic.hpp
/usr/include/boost/type_traits/cv_traits.hpp
/usr/include/boost/utility/swap.hpp
/usr/include/c++/4.7/algorithm
/usr/include/c++/4.7/bits/stl_algo.h
/usr/include/c++/4.7/bits/algorithmfwd.h
/usr/include/c++/4.7/bits/stl_heap.h
/usr/include/c++/4.7/bits/stl_tempbuf.h
/usr/include/c++/4.7/bits/stl_construct.h
/usr/include/c++/4.7/ext/alloc_traits.h
/usr/include/c++/4.7/bits/alloc_traits.h
/usr/include/c++/4.7/bits/ptr_traits.h
/usr/include/c++/4.7/random
/usr/include/c++/4.7/limits
/usr/include/c++/4.7/bits/random.h
/usr/include/c++/4.7/vector
/usr/include/c++/4.7/bits/stl_uninitialized.h
/usr/include/c++/4.7/bits/stl_vector.h
/usr/include/c++/4.7/bits/stl_bvector.h
/usr/include/c++/4.7/bits/vector.tcc
/usr/include/c++/4.7/bits/random.tcc
/usr/include/c++/4.7/numeric
/usr/include/c++/4.7/bits/stl_numeric.h
/usr/include/c++/4.7/functional
/usr/include/c++/4.7/tuple
/usr/include/c++/4.7/bits/uses_allocator.h
/usr/include/boost/utility/enable_if.hpp
/usr/include/boost/mpl/vector.hpp
/usr/include/boost/mpl/limits/vector.hpp
/usr/include/boost/mpl/vector/vector20.hpp
/usr/include/boost/mpl/vector/vector10.hpp
/usr/include/boost/mpl/vector/vector0.hpp
/usr/include/boost/mpl/vector/aux_/at.hpp
/usr/include/boost/mpl/at_fwd.hpp
/usr/include/boost/mpl/vector/aux_/tag.hpp
/usr/include/boost/mpl/aux_/config/typeof.hpp
/usr/include/boost/mpl/long.hpp
/usr/include/boost/mpl/long_fwd.hpp
/usr/include/boost/mpl/void.hpp
/usr/include/boost/mpl/aux_/type_wrapper.hpp
/usr/include/boost/mpl/vector/aux_/front.hpp
/usr/include/boost/mpl/front_fwd.hpp
/usr/include/boost/mpl/vector/aux_/push_front.hpp
/usr/include/boost/mpl/push_front_fwd.hpp
/usr/include/boost/mpl/vector/aux_/item.hpp
/usr/include/boost/mpl/next_prior.hpp
/usr/include/boost/mpl/aux_/common_name_wknd.hpp
/usr/include/boost/mpl/vector/aux_/pop_front.hpp
/usr/include/boost/mpl/pop_front_fwd.hpp
/usr/include/boost/mpl/vector/aux_/push_back.hpp
/usr/include/boost/mpl/push_back_fwd.hpp
/usr/include/boost/mpl/vector/aux_/pop_back.hpp
/usr/include/boost/mpl/pop_back_fwd.hpp
/usr/include/boost/mpl/vector/aux_/back.hpp
/usr/include/boost/mpl/back_fwd.hpp
/usr/include/boost/mpl/vector/aux_/clear.hpp
/usr/include/boost/mpl/clear_fwd.hpp
/usr/include/boost/mpl/vector/aux_/vector0.hpp
/usr/include/boost/mpl/vector/aux_/iterator.hpp
/usr/include/boost/mpl/iterator_tags.hpp
/usr/include/boost/mpl/plus.hpp
/usr/include/boost/mpl/aux_/arithmetic_op.hpp
/usr/include/boost/mpl/aux_/largest_int.hpp
/usr/include/boost/mpl/aux_/numeric_op.hpp
/usr/include/boost/mpl/numeric_cast.hpp
/usr/include/boost/mpl/apply_wrap.hpp
/usr/include/boost/mpl/aux_/has_apply.hpp
/usr/include/boost/mpl/has_xxx.hpp
/usr/include/boost/mpl/aux_/yes_no.hpp
/usr/include/boost/mpl/aux_/config/arrays.hpp
/usr/include/boost/mpl/aux_/config/has_xxx.hpp
/usr/include/boost/mpl/aux_/config/msvc_typename.hpp
/usr/include/boost/preprocessor/array/elem.hpp
/usr/include/boost/preprocessor/array/data.hpp
/usr/include/boost/preprocessor/array/size.hpp
/usr/include/boost/mpl/aux_/config/has_apply.hpp
/usr/include/boost/mpl/aux_/msvc_never_true.hpp
/usr/include/boost/mpl/aux_/preprocessed/gcc/apply_wrap.hpp
/usr/include/boost/mpl/tag.hpp
/usr/include/boost/mpl/aux_/has_tag.hpp
/usr/include/boost/mpl/aux_/numeric_cast_utils.hpp
/usr/include/boost/mpl/aux_/config/forwarding.hpp
/usr/include/boost/mpl/aux_/msvc_eti_base.hpp
/usr/include/boost/mpl/aux_/is_msvc_eti_arg.hpp
/usr/include/boost/mpl/aux_/preprocessed/gcc/plus.hpp
/usr/include/boost/mpl/minus.hpp
/usr/include/boost/mpl/aux_/preprocessed/gcc/minus.hpp
/usr/include/boost/mpl/advance_fwd.hpp
/usr/include/boost/mpl/distance_fwd.hpp
/usr/include/boost/mpl/next.hpp
/usr/include/boost/mpl/prior.hpp
/usr/include/boost/mpl/vector/aux_/O1_size.hpp
/usr/include/boost/mpl/O1_size_fwd.hpp
/usr/include/boost/mpl/vector/aux_/size.hpp
/usr/include/boost/mpl/size_fwd.hpp
/usr/include/boost/mpl/vector/aux_/empty.hpp
/usr/include/boost/mpl/empty_fwd.hpp
/usr/include/boost/mpl/vector/aux_/begin_end.hpp
/usr/include/boost/mpl/begin_end_fwd.hpp
/usr/include/boost/mpl/vector/aux_/include_preprocessed.hpp
/usr/include/boost/mpl/vector/aux_/preprocessed/typeof_based/vector10.hpp
/usr/include/boost/mpl/vector/aux_/preprocessed/typeof_based/vector20.hpp
/usr/include/boost/mpl/aux_/preprocessed/gcc/vector.hpp
/usr/include/boost/call_traits.hpp
/usr/include/boost/detail/call_traits.hpp
boost/coroutine/detail/arg_max.hpp
boost/coroutine/detail/coroutine_impl.hpp
/usr/include/boost/optional.hpp
/usr/include/boost/optional/optional.hpp
/usr/include/boost/type.hpp
/usr/include/boost/detail/reference_content.hpp
/usr/include/boost/none.hpp
/usr/include/boost/none_t.hpp
/usr/include/boost/utility/compare_pointees.hpp
/usr/include/boost/utility/in_place_factory.hpp
/usr/include/boost/utility/detail/in_place_factory_prefix.hpp
/usr/include/boost/preprocessor/punctuation/paren.hpp
/usr/include/boost/preprocessor/iteration/iterate.hpp
/usr/include/boost/preprocessor/slot/slot.hpp
/usr/include/boost/preprocessor/slot/detail/def.hpp
/usr/include/boost/preprocessor/iteration/detail/iter/forward1.hpp
/usr/include/boost/preprocessor/iteration/detail/bounds/lower1.hpp
/usr/include/boost/preprocessor/slot/detail/shared.hpp
/usr/include/boost/preprocessor/iteration/detail/bounds/upper1.hpp
/usr/include/boost/utility/detail/in_place_factory_suffix.hpp
/usr/include/boost/optional/optional_fwd.hpp
boost/coroutine/detail/argument_unpacker.hpp
/usr/include/boost/utility/result_of.hpp
/usr/include/boost/mpl/or.hpp
/usr/include/boost/mpl/aux_/preprocessed/gcc/or.hpp
/usr/include/boost/utility/detail/result_of_iterate.hpp
boost/coroutine/detail/index.hpp
boost/coroutine/detail/coroutine_accessor.hpp
boost/coroutine/detail/context_base.hpp
/usr/include/boost/detail/atomic_count.hpp
/usr/include/boost/smart_ptr/detail/atomic_count.hpp
/usr/include/boost/smart_ptr/detail/sp_has_sync.hpp
/usr/include/boost/smart_ptr/detail/atomic_count_gcc_x86.hpp
/usr/include/boost/intrusive_ptr.hpp
/usr/include/boost/smart_ptr/intrusive_ptr.hpp
/usr/include/boost/smart_ptr/detail/sp_convertible.hpp
/usr/include/boost/config/no_tr1/functional.hpp
/usr/include/boost/smart_ptr/detail/operator_bool.hpp
boost/coroutine/detail/noreturn.hpp
boost/coroutine/detail/is_callable.hpp
/usr/include/boost/mpl/logical.hpp
boost/coroutine/detail/argument_packer.hpp
boost/coroutine/detail/signature.hpp
boost/coroutine/detail/coroutine_traits.hpp
boost/coroutine/tuple_traits.hpp
boost/coroutine/detail/yield_result_type.hpp
boost/coroutine/detail/make_tuple_traits.hpp
boost/coroutine/move.hpp
/usr/include/boost/preprocessor/seq/seq.hpp
/usr/include/boost/preprocessor/seq/elem.hpp
boost/coroutine/detail/has_swap.hpp
boost/coroutine/detail/fix_result.hpp
boost/coroutine/detail/self.hpp
boost/coroutine/detail/signal.hpp
boost/coroutine/detail/future_impl.hpp
Which is more than 600 includes. The preprocessed file of just including the header has almost 75000 lines.
The reason for this is that boost makes almost no compromises. Boost code has to compile on all machines and on all compilers. It has to be easy to use and support all use cases. It must be exception safe and thread safe. And finally it also must have as little runtime cost as possible. The only thing that a boost library will compromise on is compilation time.
I think that that is a bad compromise. Compilation time is one of those things that will come back to bite you if you’re not paying attention to it. I’m convinced that you can improve your runtime performance more if you speed up your compilation time than if you do all the metaprogramming tricks that they use, simply because you can iterate better. And boost simply takes it too far.
So how do you actually implement coroutines in C++? On POSIX compliant systems you use ucontext.h:
#include <ucontext.h> #include <cstdint> #include <memory> #include <functional> struct coroutine { coroutine(const std::function<void (coroutine &)> func, size_t stack_size = SIGSTKSZ) : stack{new unsigned char[stack_size]}, func{func} { getcontext(&callee); callee.uc_link = &caller; callee.uc_stack.ss_size = stack_size; callee.uc_stack.ss_sp = stack.get(); makecontext(&callee, reinterpret_cast<void (*)()>(&coroutine_call), 2, reinterpret_cast<size_t>(this) >> 32, this); } coroutine(const coroutine &) = delete; coroutine & operator=(const coroutine &) = delete; coroutine(coroutine &&) = default; coroutine & operator=(coroutine &&) = default; void operator()() { if (returned) return; swapcontext(&caller, &callee); } operator bool() const { return !returned; } void yield() { swapcontext(&callee, &caller); } private: ucontext_t caller; ucontext_t callee; std::unique_ptr<unsigned char[]> stack; std::function<void (coroutine &)> func; bool returned = false; static void coroutine_call(uint32_t this_pointer_left_half, uint32_t this_pointer_right_half) { coroutine & this_ = *reinterpret_cast<coroutine *>((static_cast<size_t>(this_pointer_left_half) << 32) + this_pointer_right_half); this_.func(this_); this_.returned = true; } };
There you go. A complete coroutine implementation in 51 lines. The constructor allocates a stack for the coroutine and sets the context up so that it returns to the caller context after it returns. I also pass in the this pointer as the argument for the function, except I have to split it up into two integers. The coroutine_call function of the coroutine reconstructs the this pointer and calls the stored function. And then it’s just about calling swapcontext at the right time.
Here’s how to use it:
#include <coroutine.h> #include <iostream> int main() { coroutine test{[](coroutine & self) { for (int i = 0; i < 5; ++i) { std::cout << "coroutine " << i << std::endl; self.yield(); } }}; while(test) { std::cout << "main" << std::endl; test(); } return 0; }
This will print
main
coroutine 0
main
coroutine 1
main
coroutine 2
main
coroutine 3
main
coroutine 4
main
This is not thread-safe, not exception-safe and it only supports void functions without arguments. You could probably work well with this in more than 90% of the cases where you’d want coroutines.
This doesn’t run on Windows because Windows doesn’t provide the ucontext.h header. There are equivalent functions on Windows though, and boost.coroutine uses them. So if you are a Windows user you might want to use boost.coroutine, or you can rewrite the above code to use the Windows functions. I think you’d want to use fibers, but I haven’t looked into it at all.
In my next post I’ll also provide a coroutine implementation which supports arguments and return values and which is exception safe.
The major unsolved problem with using coroutines for game logic (at least in my eyes) is that you can’t serialize them. The switch statements have the advantage that 100% of the useful state, by necessity, is stored somewhere in a program-readable fashion. I can’t imagine a way to get a ucontext/fiber coroutine savable/restorable without destroying the reason that it’s clean in the first place.
Even dynamic languages, which you’d think would be amenable to saving and restoring coroutines, have a hitch: what happens if you save in one version and restore in another? What if the actual code implementing the process changes out from under you?
This approach has advantage on using stack variables. If state has to be serialized, switch-case style coroutine should be considered then.
That is a great point and I hadn’t thought of that.
Seems like coroutines are only useful for things that do not have gameplay impact, such as the streaming that Linden Lab is using it for.
But I feel like coroutines not being serializable should be a solvable problem. It’s not like you get automatic serialization when you use a state machine. You still have to write the code to store and restore every state.
I think I’d be willing to give up some convenience if it meant keeping the instruction pointer and keeping the stack.
I’m specifically thinking about writing a macro around every variable definition and around every function call. Might be worth trying, even though I think it’s very likely that it won’t work or will turn out to be a giant mess. No idea if I will ever get to it though.
After thinking about it the problem for far less time than it deserves, I can only come up with one way that it might be bearable to save/load a coroutine without actually persisting the context and call stack: record the history of inputs that describes the coroutine’s state. You could then replay the coroutine in “null output” mode, where you give it exactly the same inputs and ignore everything that it does. You’d probably also need the coroutine to enter some sort of “ground state” every once in a while, where it clears the list of recorded inputs and ends up in exactly the same state it would’ve started in.
The advantage of that is that it’s not terribly intrusive on the code itself (all you need is a “fake” mode), and it might work half decently if the coroutine’s code ever changes and you replay inputs recorded on version 1.00 against version 1.01.
The disadvantage is if your coroutine takes a continuous stream of input (position and velocity every frame, for example). That’s a lot of state data.
Everything else that I can think of is basically building a state machine in parallel to the coroutine—at which point you might as well invent some sort of domain-specific language for writing (hierarchical?) state machines to make them look more like procedural code.
You actually made me think of a mixture of the two systems, which I think might have a chance. I would still use macros to serialize all stack variables, but I would replay the coroutine from the start after loading a save game. Since everything is serialized when replaying you could figure out what your behavior should be.
For example you would implement sleep like this:
If you then restart your coroutine the next time, you would not get the full seconds value, but whatever the value was when you yielded the last time.
When continuing the coroutine after saving you will probably encounter many stored variables that aren’t needed when replaying because their block of code isn’t needed. However you can safely skip them if you check what their offset from the stack base is. If you are looking for a “float x at stack offset 120” but you encounter a “int z at stack offset 128” then it’s safe to skip that one because you are likely to run into your float x further on in the sequence of stored values. If the “int z” was at stack offset 112, then you know that something is wrong in your sequence, and somebody forgot to serialize something.
You also only ever need to store values that can still be present when you yield. So you only have to make those functions safe which get passed the “coroutine & self” parameter. You could still call all the rest of your codebase normally, because it’s impossible that you would yield from there, and because of that it’s impossible that you would need to save in the middle of that function.
I think with this you could rewrite the initial example as
The only real ugly part is the “firstrun” part around the “explode(*guy);”. Which just makes sure that you don’t re-explode him if your coroutine was saved and restarted in one of the last two sleep blocks. But you could put that in another macro, like this:
CORO_RUN_ONCE(explode(*guy));
I think this might work. I’ll give it a shot one of these days.