Recently, high performance server development based on coroutines is becoming more and more popular in the open source projects of various companies, such as libco of wechat team, libgo of Meizu and libcopp. Most of the traditional high-performance server development is based on asynchronous framework and multi-thread or multi-process model design, although this architecture has experienced for a long time and rich experience, but it has inherent shortcomings: (1). Asynchronous architectures force code logic apart, which is not conducive to normal sequential thinking and is not developer-friendly. (2) Although threads share a large amount of data relative to processes, they are relatively efficient in creating and switching. As a lightweight scheduling unit at the kernel level, switching of threads in the X86 architecture requires a large number of CPU instruction cycles to complete; At the same time, when the business growth, if increase processing capacity through increase in worker threads, but most likely to make the system resource consumption in the thread management resources and the overhead of thread scheduling, get just have the reverse effect, so the number of working in Nginx process and the number of CPU execution unit is the same, The process (thread) affinity to the CPU core can minimize the loss caused by process (thread) switching (such as cache failure, etc.); (3). Although we can sometimes pass ::sched_yield(); However, the switched thread is completely determined by the scheduling algorithm. Compared with the switched thread, it is passive. As a common production-consumer thread model, the two can only be passive and it is difficult to achieve efficient “collaboration”. (4) because of the above reasons, the switch between threads basically belongs to the passive state that the user program cannot control, so many critical areas must be explicitly protected by locking. In this environment, more lightweight coroutine development emerged and was widely used by major manufacturers. The Boost.Coroutine2 coroutine library includes packages of both stackless and stackful coroutines that are easy to use. The use of Coroutine coroutines in Boost.Asio was explained in relative detail in my previous article. The Boost.Context library (for Stackful coroutines) is the foundation for resource management and maintenance during coroutine switching. In fact, there are many ways to implement coroutines, capable large factories can manually create and maintain stack space, save and switch CPU register execution state information, these are closely related to the architecture, will also involve more assembly operations, and for the general developers want to quickly develop coroutines prototype, Existing tools such as UContext or Boost.Context are often used to assist in stack space and state management. Ucontext has a long history. The uContext_T structure is used to store the stack information, CPU execution Context, signal mask, and address of the next UContext_T structure needed by the resume. Boost.Context has been the mainstay of the C++ coroutine underlying support library in recent years, and its performance has been optimized. In a traditional threaded environment, you can store abstract state information about the current execution (stack space, stack pointer, CPU register and state register, IP instruction pointer), then suspend the current execution state, and the program’s execution flow continues elsewhere. This basic build can be used to open up user-mode threads to build more advanced operation interfaces such as coroutines. Also, since this switch is in user space, the resource consumption is minimal, and all information about stack space and execution state is kept, so functions can be nested freely. From what I’ve seen, the latest version of Boost.Context is much more updated than the previous version, abstracting the execution_context type. The internal implementation files show that the internal infrastructure still uses fcontext_t to store state. Use make_fContext, jump_fContext, and the new ontop_fContext. Those familiar with previous versions can call these interfaces directly. The latest Boost.Context relies on some new features in C++11, and Boost’s cooutine library maintains two versions of Boost.Coroutine and Boost. They were all written by Oliver Kowalke, after all. Creating execution_context allocates a context stack space with the data structure that holds the context information at the top of the stack. This data structure cannot be accessed by the execution_context environment in the design. The internal state is automatically updated and saved only when the operator() is called. Just like boost:: Thread, operator()execution_context does not support copying, only move constructs and move assignments. All execution_context requires a context-function with the following function signature:
auto execution_context(execution_context ctx, Args ... args)
Copy the code
The first argument, CTX, is fixed, indicating that it is the context to which resume is automatically switched when the current context is suspended. Typically, it is the creator and caller of the current context. The following mutable arguments are automatically passed to the execution_context::operator() function as arguments. An example of simple use of execution_context from Boost.Context
int n = 9; ctx::execution_context<int> source( [n](ctx::execution_context<int> sink, int/*not used*/ ) mutable { int a=0, b=1; while(n-- >0){ auto result = sink(a); sink = std::move(std::get<0>(result)); auto next = a + b; a = b; b = next; } return sink; }); for(int i=0; i<10; ++i) { if(source) { auto result = source(0); source = std::move( std::get<0>(result) ); std::cout << std::get<1>(result) << " "; }} // The output is 0 1 1 2 3 5 8 13 21 0 %Copy the code
The return type of this function depends on the type of the template argument to instantiate execution_context: Void instantiation of execution_context can be used to create an object if no data is passed between suspend and resume contexts but merely to control flow switching. Otherwise, the resumeer will receive STD ::tuple as a return value. The first value is the suspend context object, and the rest is a packaged return value. If you only return a single value but a different data type, consider boost:: Variant, where multiple return values are wrapped in sequence
ctx::execution_context<int, std::string> ctx1( [](ctx::execution_context<int, std::string> ctx2, int num, std::string) { std::string str; STD ::tie(ctx2, num, STR) = ctx2(num+9, 6); return std::move(ctx2); }); int i = 1; int ret_j; std::string ret_str; std::tie(ctx1, ret_j, ret_str) = ctx1(i, ""); std::cout << ret_j << "~" << ret_str << std::endl;Copy the code
If you want to execute an additional function of your own on a resumed context, you can set the first parameter to exec_ontop_arg, and then pass the function that the context needs to pass. When the resumed context is called, Arguments are passed to the specified function to execute, and the return type of the function must be STD ::tuple arguments that can be passed to the Resume context. The context switch occurs when resume uses its arguments to continue execution. This was recently introduced in the new version of Boost.Context and has the effect of adding a hook call to the previously executed Context.
ctx::execution_context<int> func1(ctx::execution_context<int> ctx, int data) {
std::cout << "func1: entered first time: " << data << std::endl;
std::tie(ctx, data) = ctx(data+1);
std::cout << "func1: entered second time: " << data << std::endl;
std::tie(ctx, data) = ctx(data+1);
std::cout << "func1: entered third time(atten): " << data << std::endl;
return ctx;
}
std::tuple<boost::context::execution_context<int>,int> func2(boost::context::execution_context<int> ctx, int data)
{
std::cout << "func2: entered: " << data << std::endl;
return std::make_tuple(std::move(ctx), -3);
}
int main(int argc, char* argv[]){
int data = 0;
ctx::execution_context< int > ctx(func1);
std::tie(ctx, data) = ctx(data+1);
std::cout << "func1: returned first time: " << data << std::endl;
std::tie(ctx, data) = ctx(data+1);
std::cout << "func1: returned second time: " << data << std::endl;
std::tie(ctx, data) = ctx(ctx::exec_ontop_arg, func2, data+1);
return 0;
}
Copy the code
The output from the code above is shown below, data+1==5 is passed to func2, then func2 wraps CTX and its parameters, and CTX gets executed using the parameters passed by Func2:
func1: entered first time: 1
func1: returned first time: 2
func1: entered second time: 3
func1: returned second time: 4
func2: entered: 5
func1: entered third time(atten): -3
Copy the code
The execution_context object is allocated a context stack when it is created and destructed automatically when context-function returns. After tracing, execute_context was introduced in Boost-1.59, In previous versions, fcontext_t was used to store and switch stack and execution state information directly by combining calls to jump_fContext () and make_fcontext(), although execution_context is now packaged more easily. But the older fcontext_T operation structure is much easier to understand, and those interested in more in-depth content can consult older versions of the documentation.
Boost.Coroutine Boost.Coroutine Boost. In fact, when someone helps you to package these complex things that depend on the underlying architecture, maybe I can make my own coroutine library one day when I have time. Libgo, libcopp, libgo, libcopp, etc.
PS: In fact, the coroutine makes the user’s thinking become synchronous, so the developer of the coroutine library should take on the task of executing the flow. Boost::Context specifies that the program is switched when execution_context::operator() is called, so the switch points are clear. Although the names are the same, sometimes they are Pointers and sometimes they are not Pointers, it is better to specify a version — 1.62.0 depth.
reference
- Windows with C++ – Lightweight Cooperative Multitasking with C++
- LIGHTWEIGHT COOPERATIVE MULTITASKING WITH BOOST.CONTEXT
- Boost Context
- Boost. Context-1.61 design model changes
- libcopp
- A low-level API for stackful context switching
- Boost.Context fcontext_t