We analyze the implementation of the swoole protocol step by step according to the execution process. The php program is as follows:
<?php go(function (){ Co::sleep(1); echo "a"; }); echo "c";
go is actually an alias for swoole_coroutine_create:
PHP_FALIAS(go, swoole_coroutine_create, arginfo_swoole_coroutine_create);
First, zif_swoole_coroutine_create is executed to create the protocol:
// Functions that are actually executed PHP_FUNCTION(swoole_coroutine_create) { zend_fcall_info fci = empty_fcall_info; zend_fcall_info_cache fci_cache = empty_fcall_info_cache; // Analytical parameters ZEND_PARSE_PARAMETERS_START(1, -1) Z_PARAM_FUNC(fci, fci_cache) Z_PARAM_VARIADIC('*', fci.params, fci.param_count) ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE); if (sw_unlikely(SWOOLE_G(req_status) == PHP_SWOOLE_CALL_USER_SHUTDOWNFUNC_BEGIN)) { zend_function *func = (zend_function *) EG(current_execute_data)->prev_execute_data->func; if (func->common.function_name && sw_unlikely(memcmp(ZSTR_VAL(func->common.function_name), ZEND_STRS("__destruct")) == 0)) { php_swoole_fatal_error(E_ERROR, "can not use coroutine in __destruct after php_request_shutdown"); RETURN_FALSE; } } long cid = PHPCoroutine::create(&fci_cache, fci.param_count, fci.params); if (sw_likely(cid > 0)) { RETURN_LONG(cid); } else { RETURN_FALSE; } } long PHPCoroutine::create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv) { if (sw_unlikely(!active)) { activate(); } // Save anonymous function parameters and execution structure php_coro_args php_coro_args; php_coro_args.fci_cache = fci_cache; php_coro_args.argv = argv; php_coro_args.argc = argc; save_task(get_task()); // Save the php stack to the current task // Create coroutine return Coroutine::create(main_func, (void*) &php_coro_args); }
php_coro_args is a structure used to store callback function information:
// The structure that holds the go() callback struct php_coro_args { zend_fcall_info_cache *fci_cache; // Anonymous function information zval *argv; // parameter uint32_t argc; // Number of parameters };
php_corutine::get_task() is used to retrieve the task currently being executed, and the initialized main_task is retrieved on the first execution:
php_coro_task PHPCoroutine::main_task = {0}; // Get the current task, not the main task static inline php_coro_task* get_task() { php_coro_task *task = (php_coro_task *) Coroutine::get_current_task(); return task ? task : &main_task; } static inline void* get_current_task() { return sw_likely(current) ? current->get_task() : nullptr; } inline void* get_task() { return task; }
save_task saves the current php stack information to the task currently in use, which is the main_task, so the information will be saved on the main_task:
void PHPCoroutine::save_task(php_coro_task *task) { save_vm_stack(task); // Save the php stack save_og(task); } inline void PHPCoroutine::save_vm_stack(php_coro_task *task) { task->bailout = EG(bailout); task->vm_stack_top = EG(vm_stack_top); // Current stack top task->vm_stack_end = EG(vm_stack_end); // Bottom of stack task->vm_stack = EG(vm_stack); // The entire stack structure task->vm_stack_page_size = EG(vm_stack_page_size); task->error_handling = EG(error_handling); task->exception_class = EG(exception_class); task->exception = EG(exception); }
The structure php_coro_task is used to save the PHP stack of the current task:
struct php_coro_task { JMP_BUF *bailout; // Internal Abnormal Use zval *vm_stack_top; // Top of stack zval *vm_stack_end; // Bottom of stack zend_vm_stack vm_stack; // Execution stack size_t vm_stack_page_size; zend_execute_data *execute_data; zend_error_handling_t error_handling; zend_class_entry *exception_class; zend_object *exception; zend_output_globals *output_ptr; /* for array_walk non-reentrancy */ php_swoole_fci *array_walk_fci; swoole::Coroutine *co; // Which coroutine does it belong to? std::stack<php_swoole_fci *> *defer_tasks; long pcid; zend_object *context; int64_t last_msec; zend_bool enable_scheduler; };
After saving the current php stack, you can start creating coroutine:
static inline long create(coroutine_func_t fn, void* args = nullptr) { return (new Coroutine(fn, args))->run(); } Coroutine(coroutine_func_t fn, void *private_data) : ctx(stack_size, fn, private_data) // Default stack size 2M { cid = ++last_cid; // Allocation of coordinator id coroutines[cid] = this; // Current object pointers are stored on global corutines static properties if (sw_unlikely(count() > peak_num)) // Update peak { peak_num = count(); } }
First, a ctx object is created, and the context object is mainly used to manage the c stack.
#define SW_DEFAULT_C_STACK_SIZE (2 *1024 * 1024) size_t Coroutine::stack_size = SW_DEFAULT_C_STACK_SIZE; ctx(stack_size, fn, private_data) Context::Context(size_t stack_size, coroutine_func_t fn, void* private_data) : fn_(fn), stack_size_(stack_size), private_data_(private_data) { end_ = false; // Whether the markup protocol has been executed or not swap_ctx_ = nullptr; stack_ = (char*) sw_malloc(stack_size_); // Allocate a memory storage c stack, default 2M if (!stack_) { swFatalError(SW_ERROR_MALLOC_FAIL, "failed to malloc stack memory."); exit(254); } swTraceLog(SW_TRACE_COROUTINE, "alloc stack: size=%u, ptr=%p", stack_size_, stack_); void* sp = (void*) ((char*) stack_ + stack_size_); // Calculate the top address of the stack, that is, the highest address ctx_ = make_fcontext(sp, stack_size_, (void (*)(intptr_t))&context_func); // Constructing context }
make_fcontext function is provided in boost.context library. It is compiled by assembly. Different platforms have different implementations. We use make_x86_64_sysv_elf_gas.S file here:
The registers used for reference are rdi, rsi, rdx, rcx, r8, r9 in turn.
make_fcontext: /* first arg of make_fcontext() == top of context-stack */ /* rax = sp */ movq %rdi, %rax /* shift address in RAX to lower 16 byte boundary */ /* rax = rax & -16 => rax = rax & (~0x10000 + 1) => rax = rax - rax%16, It's actually aligned to 16.*/ andq $-16, %rax /* reserve space for context-data on context-stack */ /* size for fc_mxcsr .. RIP + return-address for context-function */ /* on context-function entry: (RSP -0x8) % 16 == 0 */ /*lea It is the abbreviation of "load effective address". Simply put, the lea instruction can be used to assign a memory address directly to the destination operand. For example, lea eax,[ebx+8] assigns the value of ebx+8 directly to eax, rather than assigning the data in the memory address at ebx+8 to eax. On the contrary, the MOV instruction, for example, mov eax,[ebx+8] assigns the data at the memory address of ebx+8 to eax.*/ /* rax = rax - 0x48, Reserve 0x48 bytes */ leaq -0x48(%rax), %rax /* third arg of make_fcontext() == address of context-function */ /* context_func Function address at rax+0x38*/ movq %rdx, 0x38(%rax) /* save MMX control- and status-word */ stmxcsr (%rax) /* save x87 control-word */ fnstcw 0x4(%rax) /* compute abs address of label finish */ /* https://sourceware.org/binutils/docs/as/i386_002dMemory.html The x86-64 architecture adds an RIP (instruction pointer relative) addressing. This addressing mode is specified by using 'rip' as a base register. Only constant offsets are valid. For example: AT&T: '1234(%rip)', Intel: '[rip + 1234]' Points to the address 1234 bytes past the end of the current instruction. AT&T: 'symbol(%rip)', Intel: '[rip + symbol]' Points to the symbol in RIP relative way, this is shorter than the default absolute addressing. */ /* rcx = finish */ leaq finish(%rip), %rcx /* save address of finish as return-address for context-function */ /* will be entered after context-function returns */ /* finish The function address is at rax+0x40 */ movq %rcx, 0x40(%rax) /*return rax*/ ret /* return pointer to context-data */ finish: /* exit code is zero */ xorq %rdi, %rdi /* exit application */ call _exit@PLT hlt
After the make_fcontext function is executed, the memory layout used to save the context is as follows:
/**************************************************************************************** * |<- ctx_ ---------------------------------------------------------------------------------- * * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | * * ---------------------------------------------------------------------------------- * * | 0x0 | 0x4 | 0x8 | 0xc | 0x10 | 0x14 | 0x18 | 0x1c | * * ---------------------------------------------------------------------------------- * * | fc_mxcsr|fc_x87_cw| | | | * * ---------------------------------------------------------------------------------- * * ---------------------------------------------------------------------------------- * * | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | * * ---------------------------------------------------------------------------------- * * | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | * * ---------------------------------------------------------------------------------- * * | | | | context_func | * * ---------------------------------------------------------------------------------- * * ---------------------------------------------------------------------------------- * * | 16 | 17 | | * * ---------------------------------------------------------------------------------- * * | 0x40 | 0x44 | | * * ---------------------------------------------------------------------------------- * * | finish | | * * ---------------------------------------------------------------------------------- * * * ****************************************************************************************/
After the Coroutine object is instantiated, the run method starts to execute the run method. The run method stores the previous Coroutine object that executed the related method in the origin and places the current as the current object:
static sw_co_thread_local Coroutine* current; Coroutine *origin; inline long run() { long cid = this->cid; origin = current; // orign saves the original object current = this; // Currt is set to the current object ctx.swap_in(); // Swap in check_end(); return cid; }
Next is the core method of switching c stack, swap_in and swap_out. The bottom layer is also provided by boost.context library. Let's first look at importing:
bool Context::swap_in() { jump_fcontext(&swap_ctx_, ctx_, (intptr_t) this, true); return true; } // jump_x86_64_sysv_elf_gas.S jump_fcontext: /* Currently registers are pushed onto the stack. Note that there is actually another rip on rbp, because call jump_fcontext is equivalent to push rip, jmp jump_fcontext. */ /* rip Save the next instruction to execute, in this case return true after jump_fcontext */ pushq %rbp /* save RBP */ pushq %rbx /* save RBX */ pushq %r15 /* save R15 */ pushq %r14 /* save R14 */ pushq %r13 /* save R13 */ pushq %r12 /* save R12 */ /* prepare stack for FPU */ leaq -0x8(%rsp), %rsp /* test for flag preserve_fpu */ cmp $0, %rcx je 1f /* save MMX control- and status-word */ stmxcsr (%rsp) /* save x87 control-word */ fnstcw 0x4(%rsp) 1: /* store RSP (pointing to context-data) in RDI */ /* *swap_ctx_ = rsp, Save the top of the stack */ movq %rsp, (%rdi) /* restore RSP (pointing to context-data) from RSI */ /* rsp = ctx_, Here the current execution stack is pointed to the stack just built through make_fcontext */ movq %rsi, %rsp /* test for flag preserve_fpu */ cmp $0, %rcx je 2f /* restore MMX control- and status-word */ ldmxcsr (%rsp) /* restore x87 control-word */ fldcw 0x4(%rsp) 2: /* prepare stack for FPU */ leaq 0x8(%rsp), %rsp /* Restore registers to values pushed in from the new stack, which are still empty at this execution */ popq %r12 /* restrore R12 */ popq %r13 /* restrore R13 */ popq %r14 /* restrore R14 */ popq %r15 /* restrore R15 */ popq %rbx /* restrore RBX */ popq %rbp /* restrore RBP */ /* restore return-address */ /* r8 = make_fcontext(Look up at the memory layout after make_fcontext ends) */ popq %r8 /* use third arg as return-value after jump */ /* rax = this */ movq %rdx, %rax /* use third arg as first arg in context function */ /* rdi = this */ movq %rdx, %rdi /* indirect jump to context */ /* Execute context_func */ jmp *%r8
After jump_fcontext is executed, the original stack memory layout is as follows:
/**************************************************************************************** * |<-swap_ctx_ * * ---------------------------------------------------------------------------------- * * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | * * ---------------------------------------------------------------------------------- * * | 0x0 | 0x4 | 0x8 | 0xc | 0x10 | 0x14 | 0x18 | 0x1c | * * ---------------------------------------------------------------------------------- * * | fc_mxcsr|fc_x87_cw| R12 | R13 | R14 | * * ---------------------------------------------------------------------------------- * * ---------------------------------------------------------------------------------- * * | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | * * ---------------------------------------------------------------------------------- * * | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | * * ---------------------------------------------------------------------------------- * * | R15 | RBX | RBP | RIP/return true | * * ---------------------------------------------------------------------------------- * * * ****************************************************************************************/
Context_func has a parameter. this which jump_fcontext writes to rdi after execution will be used as a parameter for context_func. fn_, private_data_are the parameters passed in when constructing ctx:
void Context::context_func(void *arg) { Context *_this = (Context *) arg; _this->fn_(_this->private_data_); // main_func(php_coro_args) _this->end_ = true; _this->swap_out(); }
The main_func assigns an execution stack to the current coroutine, binds it to the Coroutine just instantiated, and then executes the callback function in go(). The original function is too long. Only some key points are retained here:
void PHPCoroutine::main_func(void *arg) { // Create a new vmstack on EG to execute the callback function in go(), and the previous execution stack has been saved on main_task vm_stack_init(); call = (zend_execute_data *) (EG(vm_stack_top)); task = (php_coro_task *) EG(vm_stack_top); EG(vm_stack_top) = (zval *) ((char *) call + PHP_CORO_TASK_SLOT * sizeof(zval)); // Reserve a place for task call = zend_vm_stack_push_call_frame(call_info, func, argc, object_or_called_scope); // Allocate stack space for parameters EG(bailout) = NULL; EG(current_execute_data) = call; // Excute_data is set to the callback function currently to be executed EG(error_handling) = EH_NORMAL; EG(exception_class) = NULL; EG(exception) = NULL; save_vm_stack(task); // Save vmstack to the current task record_last_msec(task); // Recording time task->output_ptr = NULL; task->array_walk_fci = NULL; task->co = Coroutine::get_current(); // Record the current coroutine task->co->set_task((void *) task); // coroutine binds to current task task->defer_tasks = nullptr; task->pcid = task->co->get_origin_cid(); // Record a co-process id task->context = nullptr; task->enable_scheduler = 1; if (EXPECTED(func->type == ZEND_USER_FUNCTION)) { ZVAL_UNDEF(retval); // TODO: enhancement it, separate execute data is necessary, but we lose the backtrace // TODO EG(current_execute_data) = NULL; zend_init_func_execute_data(call, &func->op_array, retval); zend_execute_ex(EG(current_execute_data)); // Execute opcode in callback } else /* ZEND_INTERNAL_FUNCTION */ { ZVAL_NULL(retval); call->prev_execute_data = NULL; call->return_value = NULL; /* this is not a constructor call */ execute_internal(call, retval); zend_vm_stack_free_args(call); } }
Next, the opcode generated by the user callback function is executed. When the user callback function is executed to Co::sleep(1), the System::sleep(seconds) is called. In this case, a timing event is registered for the current coroutine. The callback function is sleep_timeout:
int System::sleep(double sec) { Coroutine* co = Coroutine::get_current_safe(); // Get the current coroutine if (swoole_timer_add((long) (sec * 1000), SW_FALSE, sleep_timeout, co) == NULL) // Add a timing event to the current couroutine { return -1; } co->yield(); // switch return 0; } // Callback for Timing Event Registration static void sleep_timeout(swTimer *timer, swTimer_node *tnode) { ((Coroutine *) tnode->data)->resume(); }
The yield function is responsible for switching between php stack and c stack
void Coroutine::yield() { SW_ASSERT(current == this || on_bailout != nullptr); state = SW_CORO_WAITING; if (sw_likely(on_yield)) { on_yield(task); // php stack switching } current = origin; // Switch the current coprocess to the previous one ctx.swap_out(); // c stack switching }
Let's first look at the handover of php stack. on_yield is a function that has been registered at the time of initialization.
void PHPCoroutine::init() { Coroutine::set_on_yield(on_yield); Coroutine::set_on_resume(on_resume); Coroutine::set_on_close(on_close); } void PHPCoroutine::on_yield(void *arg) { php_coro_task *task = (php_coro_task *) arg; // Current task php_coro_task *origin_task = get_origin_task(task); // Get the last task swTraceLog(SW_TRACE_COROUTINE,"php_coro_yield from cid=%ld to cid=%ld", task->co->get_cid(), task->co->get_origin_cid()); save_task(task); // Save the current task restore_task(origin_task); // Restore the previous task }
Getting the last task can restore EG through the execution information saved above. The program is very simple, just exchange vmstack and current_execute_data back.
void PHPCoroutine::restore_task(php_coro_task *task) { restore_vm_stack(task); restore_og(task); } inline void PHPCoroutine::restore_vm_stack(php_coro_task *task) { EG(bailout) = task->bailout; EG(vm_stack_top) = task->vm_stack_top; EG(vm_stack_end) = task->vm_stack_end; EG(vm_stack) = task->vm_stack; EG(vm_stack_page_size) = task->vm_stack_page_size; EG(current_execute_data) = task->execute_data; EG(error_handling) = task->error_handling; EG(exception_class) = task->exception_class; EG(exception) = task->exception; if (UNEXPECTED(task->array_walk_fci && task->array_walk_fci->fci.size != 0)) { memcpy(&BG(array_walk_fci), task->array_walk_fci, sizeof(*task->array_walk_fci)); task->array_walk_fci->fci.size = 0; } }
At this point, the php stack execution state has been restored to the main_task when the go() function was just called, and then see how the c stack switch is handled:
bool Context::swap_out() { jump_fcontext(&ctx_, swap_ctx_, (intptr_t) this, true); return true; }
Recall the swap_in function, swap_ctx_saves the RSP when swap_in is executed, and ctx_saves the top position of the stack initialized by make_fcontext. Look again at jump_fcontext execution:
// jump_x86_64_sysv_elf_gas.S jump_fcontext: /* Currently registers are pushed onto the stack. Note that there is actually another rip on rbp, because call jump_fcontext is equivalent to push rip, jmp jump_fcontext. */ /* rip Save the next instruction to execute, in this case return true after jump_fcontext in swap_out */ pushq %rbp /* save RBP */ pushq %rbx /* save RBX */ pushq %r15 /* save R15 */ pushq %r14 /* save R14 */ pushq %r13 /* save R13 */ pushq %r12 /* save R12 */ /* prepare stack for FPU */ leaq -0x8(%rsp), %rsp /* test for flag preserve_fpu */ cmp $0, %rcx je 1f /* save MMX control- and status-word */ stmxcsr (%rsp) /* save x87 control-word */ fnstcw 0x4(%rsp) 1: /* store RSP (pointing to context-data) in RDI */ /* *ctx_ = rsp, Save the top of the stack */ movq %rsp, (%rdi) /* restore RSP (pointing to context-data) from RSI */ /* rsp = swap_ctx_, Here, the current execution stack is pointed to rsp when swap_in was previously executed */ movq %rsi, %rsp /* test for flag preserve_fpu */ cmp $0, %rcx je 2f /* restore MMX control- and status-word */ ldmxcsr (%rsp) /* restore x87 control-word */ fldcw 0x4(%rsp) 2: /* prepare stack for FPU */ leaq 0x8(%rsp), %rsp /* Restore registers to the state when swap_in is executed */ popq %r12 /* restrore R12 */ popq %r13 /* restrore R13 */ popq %r14 /* restrore R14 */ popq %r15 /* restrore R15 */ popq %rbx /* restrore RBX */ popq %rbp /* restrore RBP */ /* restore return-address */ /* r8 = Context::swap_in::return true */ popq %r8 /* use third arg as return-value after jump */ /* rax = this */ movq %rdx, %rax /* use third arg as first arg in context function */ /* rdi = this */ movq %rdx, %rdi /* indirect jump to context */ /* Then the last swap_in location continues to execute */ jmp *%r8
By this time, both php and c stacks have been restored to the state of executing swap_in, and the code continues to execute in the state at that time:
bool Context::swap_in() { jump_fcontext(&swap_ctx_, ctx_, (intptr_t) this, true); return true; // Start from here and go back to calling its function } inline long run() { long cid = this->cid; origin = current; current = this; ctx.swap_in(); check_end(); // Check whether the process has been completed and need to be cleaned up after execution. return cid; } static inline long create(coroutine_func_t fn, void* args = nullptr) { return (new Coroutine(fn, args))->run(); } long PHPCoroutine::create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv) { if (sw_unlikely(!active)) { activate(); } php_coro_args php_coro_args; php_coro_args.fci_cache = fci_cache; php_coro_args.argv = argv; php_coro_args.argc = argc; save_task(get_task()); return Coroutine::create(main_func, (void*) &php_coro_args); } PHP_FUNCTION(swoole_coroutine_create) { long cid = PHPCoroutine::create(&fci_cache, fci.param_count, fci.params); if (sw_likely(cid > 0)) { RETURN_LONG(cid); // Return the coordinator id } else { RETURN_FALSE; } }
php also continues to execute code after go(), which is equivalent to skipping the code behind sleep.
<?php go(function (){ Co::sleep(1); echo "a"; }); echo "c"; // Starting from here, continue to implement
When will the code behind sleep execute? We noticed that when we called sleep, we registered a timed event sleep_timeout with reactor, which triggered the callback function when the timed time arrived.
// Callback for Timing Event Registration static void sleep_timeout(swTimer *timer, swTimer_node *tnode) { ((Coroutine *) tnode->data)->resume(); } // Restore the entire execution environment void Coroutine::resume() { SW_ASSERT(current != this); if (sw_unlikely(on_bailout)) { return; } state = SW_CORO_RUNNING; if (sw_likely(on_resume)) { on_resume(task); // Restore php execution status } origin = current; current = this; ctx.swap_in(); // Restore c stack check_end(); } // Restore task void PHPCoroutine::on_resume(void *arg) { php_coro_task *task = (php_coro_task *) arg; php_coro_task *current_task = get_task(); save_task(current_task); // Save the current task restore_task(task); // Recovery mission record_last_msec(task); swTraceLog(SW_TRACE_COROUTINE,"php_coro_resume from cid=%ld to cid=%ld", Coroutine::get_current_cid(), task->co->get_cid()); }
Here, the php execution stack is restored to the state when the sleep is invoked, then swap_in is invoked, and C stack is restored to the state when the previous swap_out is invoked. System::sleep method is finished, zend_vm continues to execute opcode after the sleep.
<?php go(function (){ Co::sleep(1); echo "a"; // Starting from here, continue to implement }); echo "c";
After the opcode in the current callback is fully executed, the PHPCoroutine::main_func function executes the previously registered defer once, in FILO order, and then cleans up the resources.
if (task->defer_tasks) { std::stack<php_swoole_fci *> *tasks = task->defer_tasks; while (!tasks->empty()) { php_swoole_fci *defer_fci = tasks->top(); tasks->pop(); // FILO defer_fci->fci.param_count = 1; defer_fci->fci.params = retval; if (UNEXPECTED(sw_zend_call_function_anyway(&defer_fci->fci, &defer_fci->fci_cache) != SUCCESS)) // call defer callback { php_swoole_fatal_error(E_WARNING, "defer callback handler error"); } sw_zend_fci_cache_discard(&defer_fci->fci_cache); efree(defer_fci); } delete task->defer_tasks; task->defer_tasks = nullptr; } // resources release zval_ptr_dtor(retval); if (fci_cache.object) { OBJ_RELEASE(fci_cache.object); } if (task->context) { OBJ_RELEASE(task->context); }
After execution of main_func, go back to the Context::context_func method, mark the current collaboration as finished, do swap_out again and go back to the place just swap_in, that is, the resume method, then check whether the wake-up process has been executed, check only need to determine the end_attribute.
void Context::context_func(void *arg) { Context *_this = (Context *) arg; _this->fn_(_this->private_data_); // main_func(closure) completed _this->end_ = true; // The current consortium is marked as terminated _this->swap_out(); } void Coroutine::resume() { SW_ASSERT(current != this); if (sw_unlikely(on_bailout)) { return; } state = SW_CORO_RUNNING; if (sw_likely(on_resume)) { on_resume(task); } origin = current; current = this; ctx.swap_in(); // reduction check_end(); // Check if the consortium is over } inline void check_end() { if (ctx.is_end()) { close(); } } inline bool is_end() { return end_; }
The close method cleans up the vm_stack created for this collaboration and cuts back to main_task
void Coroutine::close() { SW_ASSERT(current == this); state = SW_CORO_END; if (on_close) { on_close(task); } current = origin; coroutines.erase(cid); // Remove the current protocol delete this; } void PHPCoroutine::on_close(void *arg) { php_coro_task *task = (php_coro_task *) arg; php_coro_task *origin_task = get_origin_task(task); vm_stack_destroy(); // Destroy vm_stack restore_task(origin_task); // Restore main_task }
At this point, the whole life cycle of the consortium is over.