Article source: Click the open link
What is a coroutine
A coroutine is a non-priority subroutine scheduling component that allows a subroutine to suspend recovery at a specific place.
Threads are contained in processes and coroutines are contained in threads. A thread can have as many coroutines as it wants as long as there is enough memory, but only one coroutine can be running at any one time, and multiple coroutines share the computer resources allocated by that thread.
Why do we need coroutines
Simple introduction
In practical terms, coroutines allow us to write synchronous code logic while doing asynchronous things, avoiding callback nesting and making code logical. code like this:
co(function*(next){ let [err,data]=yield fs.readFile("./test.txt",next); [err]=yield fs.appendFile("./test2.txt",data,next); // Write files asynchronously //.... }) ()Copy the code
An asynchronous operation is an operation whose result is not immediately apparent after an asynchronous instruction is executed. Completion of its instructions does not mean completion of the operation.
Coroutines are the result of extreme performance and elegant code structure.
A bit of history
At first, people like to program synchronously, and then they find that there are a bunch of threads that can’t do it because the I/O is stuck, and it’s a waste of resources.
Then it goes asynchronous (select,epoll,kqueue,etc), hands off the I/O operations to the kernel thread, and registers a callback function itself to process the final result.
However, as the project grows larger, the code structure becomes unclear. Here is a small example.
async_func1("hello world",func(){ async_func2("what's up?" ,func(){ async_func2("oh ,friend!" ,func(){ //todo something }) }) })Copy the code
So they invented coroutines, wrote synchronous code, and enjoyed the performance advantages of asynchrony.
Resources needed to run the program:
- cpu
- memory
- I/O (files, network, disk (memory access is not at the same level and is ignored))
How to implement coroutines (c++ and node.js implementations)
Libco a C++ coroutine library implementation
Libco is an open source C++ coprogramming library from Tencent. As the basic library of wechat background, libco has withstood the actual test. Project address: github.com/Tencent/lib…
Personal source reading project: github.com/yyrdl/libco… (Unfinished)
There are 11 libco source files, one in assembly code and the rest in C++, which are relatively easy to read.
Implementation of coroutines in C++ to solve the following problems:
- When to suspend coroutines? When to wake up coroutines?
- How do I suspend and wake up coroutines, and how do I protect the context in which coroutines run?
- How do you encapsulate asynchronous operations?
Preliminary knowledge preparation
- Modern operating system is a time-sharing operating system, the basic unit of resource allocation is process, the basic unit of CPU scheduling is thread.
- C++ programs run on a runtime stack, and a single function call generates a record on the stack
- Runtime memory space is divided into global variable area (storing functions, global variables), stack area, heap area. Stack memory is allocated from high address to low address, and heap is allocated from low address to high address.
- The next instruction address exists in the instruction register IP, ESP to the current top stack address, and EBP to the current active stack frame base address.
- When a function call occurs, the parameters are pushed from right to left, the address is returned, the value of the current EBP register is pushed, the space required by the local variable of the current function is allocated in the stack area, and the value of the ESP register is modified.
- The context of a coroutine contains the values in its stack area and registers.
When to suspend and wake up the coroutine?
As stated at the beginning of the introduction, coroutines are designed to take advantage of asynchrony, which prevents IO operations from blocking threads. The time when the coroutine is suspended should be when the current coroutine initiates an asynchronous operation, and the wake up should be when the other coroutine exits and its asynchronous operation completes.
How do I suspend and wake up coroutines, and how do I protect the context in which coroutines run?
The time when the coroutine initiates an asynchronous operation is the time when the coroutine is suspended. To ensure that the coroutine works properly when it wakes up, its runtime context needs to be properly saved and restored.
So the operation steps here are:
- Saves the context of the current coroutine (run stack, return address, register state)
- Sets the entry instruction address of the coroutine to wake up to the IP register
- Restores the context of the coroutine to be awakened
This part of the operation corresponding source code:
Globl coctx_swap// define the function name exposed by this part of the assembly code #if! defined( __APPLE__ ) .type coctx_swap, @function #endif coctx_swap: #if defined(__i386__) leal 4(%esp), %eax //sp R[eax]=R[esp]+4 R[eax] %esp // R[esp]=Mem[R[esp]+4] Set ESP to &(curr-> CTX) Now esp should point to reg[0] leal 32(%esp), %esp //parm a: ®s[7] + sizeof(void*) Push is based on the value of ESP. If you push a value, the value of ESP is subtracted by one unit. Since CTX is in the heap, you should point ESP to REg [7]. Then from eax to -4(%eax)push // save register value to stack, Pushl %eax //esp -> PARm a pusHL % eBP PUShl % ESI pusHL %edi PusHL %edx Pushl %ecx pushl %ebx pushL-4 (%eax) Movl 4(%eax), save to coctx_t->regs[0] %esp //parm b -> ®s[0] // Switch ESP to the start address of the target routine CTX in the stack, which corresponds to regs[0]. Popl %eax //ret func ADDR adds one unit to ESP Regs [1] popl %ecx // regs[2] popl %edx // regs[3] popl %edi // regs[4] Popl % ESI // regs[5] popl %ebp // regs[6] popl %esp // regs[7 %eax, %eax // returns, which switches to the target routine. Code after coctx_swap in C++ will not execute ret #elif immediatelyCopy the code
This part of the code just does the register part of the operation. Dependencies are defined in the coctx.h file:
struct coctx_t { #if defined(__i386__) void *regs[ 8 ]; / / a 32-bit machine, followed by: ret, ebx, ecx, edx, edi, esi, ebp, eax # else void * regs, [14]. #endif size_t ss_size; // Space size char *ss_sp; //ESP };Copy the code
The coctx_swap function is called only in the co_swap file co_routine. CPP.
The operation to save the run stack is described in the co_swap function before coctx_swap is called. This is done by taking the current top address (char C; Esp =&c), if it is not the shared stack model, clean env, if it is, determine whether the shared stack is occupied, then apply for memory from the heap for storage, and then allocate the shared stack.
Note that the stack area of the libco runtime is not the stack area in the traditional sense, it actually comes from the heap area.
How do you encapsulate asynchronous operations?
See this part of the code:
- co_hook_sys_call.cpp
- co_routine.cpp
- co_epoll.cpp
- co_epoll.h
The core idea is that the original I/O interface of hook system, such as socket() function, is combined with epoll(kqueue), and a CO_eventloop is used for unified management. When a coroutine is found to initiate an asynchronous operation, it is suspended and put into a waiting queue, and the coroutine that has been completed by other asynchronous operations is awakened. You can contact event_loop in libevent. The difference is that one operates on the stack area and the register recovery coroutine, and the other calls the bound callback function.
Coroutines in Node.js
Advantages of Node.js:
- Node.js is naturally asynchronous (libuv)
- Javascript’s closure feature does the job of saving context
What we need to do:
- Implementing synchronous programming
Attach the code from the beginning of the article:
const fs=require("fs"); const co=require("zco"); co(function*(next){ let [err,data]=yield fs.readFile("./test.txt",next); [err]=yield fs.appendFile("./test2.txt",data,next); // Write files asynchronously //.... }) ()Copy the code
The Generator in JS
Generator is an iterator Generator and is key to implementing coroutines in Node.js.
let gen=function *() {
console.log("ok1");
var a=yield 1;
console.log("a:"+a);
var b=yield 2;
console.log("b:"+b);
}
var iterator=gen();
console.log("ok2");
console.log(iterator.next(100));
console.log(iterator.next(101));
console.log(iterator.next(102));
Copy the code
Output:
ok2
ok1
{ value: 1, done: false }
a:101
{ value: 2, done: false }
b:102
{ value: undefined, done: true }
Copy the code
From here we can see the order of execution and how the values change. Iterator.next () returns the value of the expression after yield, and the value of the variable before yield is the value passed in by iterator.next. Through this feature, a coroutine can be packaged properly.
The following is the source code of zCO module, project address: github.com/yyrdl/zco:
/** * Created by yyrdl on 2017/3/14. */ var slice = Array.prototype.slice; var co = function (gen) { var iterator, callback = null, hasReturn = false; var _end = function (e, v) { callback && callback(e, v); //I shoudn't catch the error throwed by user's callback if(callback==null&&e){//the error should be throwed if no handler instead of catching silently throw e; } } var run=function(arg){ try { var v = iterator.next(arg); hasReturn = true; v.done && _end(undefined, v.value); } catch (e) { _end(e); } } var nextSlave = function (arg) { hasReturn = false; run(arg); } var next = function () { var arg = slice.call(arguments); if (! hasReturn) {//support fake async operation,avoid error: "Generator is already running" setTimeout(nextSlave, 0, arg); } else { nextSlave(arg); } } if ("[object GeneratorFunction]" === Object.prototype.toString.call(gen)) {//todo: support other Generator implements iterator = gen(next); } else { throw new TypeError("the arg of co must be generator function") } var future = function (cb) { if ("function" == typeof cb) { callback = cb; } run(); } return future; } module.exports = co;Copy the code