4.0 协程实现原理

内存栈

4.0版本使用了PHP+C的双栈模式。创建协程时会创建一个C栈,默认尺寸为2M,创建一个PHP栈,默认为8K

C栈主要用于保存底层函数调用的局部变量数据,用于解决call_user_funcarray_mapC函数调用在协程切换时未能还原的问题。4.0版本无论如何切换协程,底层总是能正确地切换回原先的C函数栈帧继续向下执行。

C栈分配的2M内存,使用了虚拟内存,并不会分配实际内存

4.0底层还支持了嵌套关系,在协程内创建子协程,子协程挂起时仍然可以恢复父协程的执行。

底层最大允许128层嵌套

  1. Context::Context(size_t stack_size, coroutine_func_t fn, void* private_data) :
  2. fn_(fn), stack_size_(stack_size), private_data_(private_data)
  3. {
  4. protect_page_ = 0;
  5. end = false;
  6. swap_ctx_ = NULL;
  7. stack_ = (char*) sw_malloc(stack_size_);
  8. swDebug("alloc stack: size=%u, ptr=%p.", stack_size_, stack_);
  9. }

PHP栈主要保存PHP函数调用的局部变量数据,主要是zval结构体,PHP中标量类型,如整型、浮点型、布尔型等是直接保存在zval结构体内的,而objectstringarray是使用引用计数管理,在堆上存储的。8KPHP栈足以保存整个函数调用的局部变量。

  1. static inline void sw_vm_stack_init(void)
  2. {
  3. uint32_t size = COROG.stack_size;
  4. zend_vm_stack page = (zend_vm_stack) emalloc(size);
  5. page->top = ZEND_VM_STACK_ELEMENTS(page);
  6. page->end = (zval*) ((char*) page + size);
  7. page->prev = NULL;
  8. EG(vm_stack) = page;
  9. EG(vm_stack)->top++;
  10. EG(vm_stack_top) = EG(vm_stack)->top;
  11. EG(vm_stack_end) = EG(vm_stack)->end;
  12. }

协程切换

C栈切换使用了boost.context 1.60汇编代码,用于保存寄存器,切换指令序列。主要是jump_fcontext这个ASM函数提供。PHP栈的切换是跟随C栈切同步进行的。底层会切换EG(vm_stack)使得PHP恢复到正确的PHP函数栈帧。4.0.2版本还增加了ob输出缓存区的切换,ob_start等操作也可以用于协程。

boost.context汇编切换协程栈的效率非常高,经过测试每秒可完成2亿次切换
某些平台下不支持boost.context汇编,底层将使用ucontext

性能对比

调用栈切换

  1. int sw_coro_resume(php_context *sw_current_context, zval *retval, zval *coro_retval)
  2. {
  3. coro_task *task = SWCC(current_task);
  4. resume_php_stack(task);
  5. if (EG(current_execute_data)->prev_execute_data->opline->result_type != IS_UNUSED && retval)
  6. {
  7. ZVAL_COPY(SWCC(current_coro_return_value_ptr), retval);
  8. }
  9. if (OG(handlers).elements)
  10. {
  11. php_output_deactivate();
  12. if (!SWCC(current_coro_output_ptr))
  13. {
  14. php_output_activate();
  15. }
  16. }
  17. if (SWCC(current_coro_output_ptr))
  18. {
  19. memcpy(SWOG, SWCC(current_coro_output_ptr), sizeof(zend_output_globals));
  20. efree(SWCC(current_coro_output_ptr));
  21. SWCC(current_coro_output_ptr) = NULL;
  22. }
  23. swTraceLog(SW_TRACE_COROUTINE, "cid=%d", task->cid);
  24. coroutine_resume_naked(task->co);
  25. if (unlikely(EG(exception)))
  26. {
  27. if (retval)
  28. {
  29. zval_ptr_dtor(retval);
  30. }
  31. zend_exception_error(EG(exception), E_ERROR TSRMLS_CC);
  32. }
  33. return CORO_END;
  34. }

协程调度

4.0协程实现中,主协程即为Reactor协程,负责整个EventLoop的运行。主协程实现事件监听,在IO事件完成后唤醒其他工作协程。

协程挂起

在工作协程中执行一些IO操作时,底层会将IO事件注册到EventLoop,并让出执行权。

  • 嵌套创建的非初代协程,会逐个让出到父协程,直到回到主协程
  • 在主协程上创建的初代协程,会立即回到主协程
  • 主协程的Reactor会继续处理IO事件、Wait监听新事件(epoll_wait

初代协程是在EventLoop内直接创建的协程,例如OnRecive回调方法中的内置协程就是初代协程

协程恢复

当主协程的Reactor接收到新的IO事件,底层会挂起主协程,并恢复IO事件对应的工作协程。该工作协程挂起或退出时,会再次回到主协程。