原文链接:
https://github.com/fupengfei058/blog/blob/master/swoole2.0%E5%8D%8F%E7%A8%8B%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md
关于协程,你可能看的最多的就是这样一句话“协程就是用户态的线程”。
要理解是什么是“用户态的线程”,必然就要先理解什么是“内核态的线程”。 内核态的线程是由操作系统来进行调度的,在切换线程上下文时,要先保存上一个线程的上下文,然后执行下一个线程,当条件满足时,切换回上一个线程,并恢复上下文。 协程也是如此,只不过,用户态的线程不是由操作系统来调度的,而是由程序员来调度的,是在用户态的。
Swoole2.0基于setjmp、longjmp 实现,在进行协程切换时会自动保存Zend VM的内存状态(主要是EG全局内存和vm stack)。主要用于高并发IO的场景,同时并发执行大量IO操作,目前支持 Redis,MySQL,TCP/UDP Client,HttpClient 4种IO操作。
swoole_coroutine.c的头部定义了一个全局变量COROG,它的结构为:
typedef struct _coro_global
{
uint32_t coro_num;
uint32_t max_coro_num;
uint32_t stack_size;
zend_vm_stack origin_vm_stack;
...
zval *origin_vm_stack_top;
zval *origin_vm_stack_end;
zval *allocated_return_value_ptr;
...
} coro_global;
该变量是用于存储协程的基本信息如开启的协程数量。此外,swoole_coroutine.c中还定义了一个很重要的宏叫做SWCC,用于保存当前协程的详细信息,我们知道Swoole为每个协程都分配了空间,用于保存协程切换时的状态信息,进行协程切换时会自动保存Zend VM的内存状态。那么所谓的状态信息就是保存于SWCC这个宏当中,回调函数执行完后释放。
接下来我们介绍swoole协程中几个比较重要的动作。
创建协程
int sw_coro_create(zend_fcall_info_cache *fci_cache, zval **argv, int argc, zval *retval, void *post_callback, void* params)
{
//为回调函数的执行做一些准备工作
...
COROG.allocated_return_value_ptr = retval;
EG(current_execute_data) = NULL;
zend_init_execute_data(call, op_array, retval);
++COROG.coro_num;
COROG.current_coro->cid = cid;
COROG.current_coro->start_time = time(NULL);
COROG.current_coro->function = NULL;
COROG.current_coro->post_callback = post_callback;
COROG.current_coro->post_callback_params = params;
COROG.require = 1;
int coro_status;
//协程切换基于setjmp,longjmp(不了解这两个函数的可以类比理解成goto)
if (!setjmp(*swReactorCheckPoint))
{
zend_execute_ex(call);
coro_close(TSRMLS_C);
swTraceLog(SW_TRACE_COROUTINE, "[CORO_END] Create the %d coro with stack. heap size: %zu", COROG.coro_num, zend_memory_usage(0));
coro_status = CORO_END;
}
else
{
//让出协程cpu执行权
coro_status = CORO_YIELD;
}
COROG.require = 0;
return coro_status;
}
让出执行权
sw_inline void coro_yield()
{
SWOOLE_GET_TSRMLS;
#if PHP_MAJOR_VERSION >= 7
EG(vm_stack) = COROG.origin_vm_stack;
EG(vm_stack_top) = COROG.origin_vm_stack_top;
EG(vm_stack_end) = COROG.origin_vm_stack_end;
#else
EG(argument_stack) = COROG.origin_vm_stack;
EG(current_execute_data) = COROG.origin_ex;
#endif
//和setjmp对应
longjmp(*swReactorCheckPoint, 1);
}
保存协程上下文信息
sw_inline php_context *sw_coro_save(zval *return_value, php_context *sw_current_context)
{
//将协程上下文信息保存于上面提及的SWCC这个宏
SWCC(current_coro_return_value_ptr) = return_value;
SWCC(current_execute_data) = EG(current_execute_data);
SWCC(current_vm_stack) = EG(vm_stack);
SWCC(current_vm_stack_top) = EG(vm_stack_top);
SWCC(current_vm_stack_end) = EG(vm_stack_end);
SWCC(current_task) = COROG.current_coro;
SWCC(allocated_return_value_ptr) = COROG.allocated_return_value_ptr;
return sw_current_context;
}
恢复协程
int sw_coro_resume(php_context *sw_current_context, zval *retval, zval *coro_retval)
{
//从SWCC恢复到EG(关于EG:这是PHP内核中十分重要的一个宏,executor_globals,是Zend执行器相关的全局变量。)
EG(vm_stack) = SWCC(current_vm_stack);
EG(vm_stack_top) = SWCC(current_vm_stack_top);
EG(vm_stack_end) = SWCC(current_vm_stack_end);
zend_execute_data *current = SWCC(current_execute_data);
if (ZEND_CALL_INFO(current) & ZEND_CALL_RELEASE_THIS)
{
zval_ptr_dtor(&(current->This));
}
zend_vm_stack_free_args(current);
zend_vm_stack_free_call_frame(current);
EG(current_execute_data) = current->prev_execute_data;
COROG.current_coro = SWCC(current_task);
COROG.require = 1;
#if PHP_MINOR_VERSION < 1
EG(scope) = EG(current_execute_data)->func->op_array.scope;
#endif
COROG.allocated_return_value_ptr = SWCC(allocated_return_value_ptr);
if (EG(current_execute_data)->opline->result_type != IS_UNUSED)
{
ZVAL_COPY(SWCC(current_coro_return_value_ptr), retval);
}
EG(current_execute_data)->opline++;
int coro_status;
//设置跳转点,方便在执行过程中再遇到异步IO操作,进行跳转。
if (!setjmp(*swReactorCheckPoint))
{
//coro exit
zend_execute_ex(EG(current_execute_data) TSRMLS_CC);
coro_close(TSRMLS_C);
coro_status = CORO_END;
}
else
{
//coro yield
coro_status = CORO_YIELD;
}
COROG.require = 0;
if (unlikely(coro_status == CORO_END && EG(exception)))
{
sw_zval_ptr_dtor(&retval);
zend_exception_error(EG(exception), E_ERROR TSRMLS_CC);
}
return coro_status;
}
协程关闭
sw_inline void coro_close(TSRMLS_D)
{
//释放内存空间,将COROG.coro_num减一
swTraceLog(SW_TRACE_COROUTINE, "Close coroutine id %d", COROG.current_coro->cid);
if (COROG.current_coro->function)
{
sw_zval_free(COROG.current_coro->function);
COROG.current_coro->function = NULL;
}
free_cidmap(COROG.current_coro->cid);
efree(EG(vm_stack));
efree(COROG.allocated_return_value_ptr);
EG(vm_stack) = COROG.origin_vm_stack;
EG(vm_stack_top) = COROG.origin_vm_stack_top;
EG(vm_stack_end) = COROG.origin_vm_stack_end;
--COROG.coro_num;
COROG.current_coro = NULL;
swTraceLog(SW_TRACE_COROUTINE, "closing coro and %d remained. usage size: %zu. malloc size: %zu", COROG.coro_num, zend_memory_usage(0), zend_memory_usage(1));
}
这是从swoole文档找的一个协程客户端demo:
$client = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
if (!$client->connect('127.0.0.1', 9501, 0.5))
{
exit("connect failed. Error: {$client->errCode}\n");
}
$client->send("hello world\n");
echo $client->recv();
$client->close();
我们分析一下它的行为,源码在swoole_client_coro.c。
首先$client = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP)实例化对象的时候触发一个构造函数,在这里进行协程栈的初始化:
static PHP_METHOD(swoole_client_coro, __construct)
{
...
//不支持长连
int client_type = php_swoole_socktype(type);
if (client_type < SW_SOCK_TCP || client_type > SW_SOCK_UNIX_STREAM)
{
swoole_php_fatal_error(E_ERROR, "Unknown client type '%d'.", client_type);
}
zend_update_property_long(swoole_client_coro_class_entry_ptr, getThis(), ZEND_STRL("type"), type TSRMLS_CC);
//init
swoole_set_object(getThis(), NULL);
//开辟内存空间
swoole_client_coro_property *client_coro_property = emalloc(sizeof(swoole_client_coro_property));
bzero(client_coro_property, sizeof(swoole_client_coro_property));
client_coro_property->iowait = SW_CLIENT_CORO_STATUS_CLOSED;
swoole_set_property(getThis(), client_coro_property_coroutine, client_coro_property);
php_context *sw_current_context = emalloc(sizeof(php_context));
sw_current_context->onTimeout = NULL;
...
sw_current_context->coro_params = *getThis();
...
RETURN_TRUE;
}
当客户端发起一个连接请求$client->connect('127.0.0.1', 9501, 0.5),swoole将保存PHP上下文信息,并让出cpu执行权,待确认连接成功后,触发epoll事件,然后协程切换,恢复PHP上下文信息,返回结果,继续执行PHP代码
static PHP_METHOD(swoole_client_coro, connect)
{
...
swoole_set_object(getThis(), cli);
...
zval *zobject = getThis();
cli->object = zobject;
...
swoole_client_coro_property *ccp = swoole_get_property(getThis(), 1);
sw_copy_to_stack(cli->object, ccp->_object);
...
//nonblock async
if (cli->connect(cli, host, port, timeout, sock_flag) < 0)
{
...
}
...
php_context *sw_current_context = swoole_get_property(getThis(), 0);
coro_save(sw_current_context);
coro_yield();
}
client向server发送数据$client->send("hello world\n"):
static PHP_METHOD(swoole_client_coro, send)
{
...
swClient *cli = client_coro_get_ptr(getThis());
...
int ret = cli->send(cli, data, data_len, flags);
if (ret < 0)
{
...
}
...
}
客户端接收数据$client->recv(),recv也是常见的阻塞IO,因此这里同样会保存当前协程的上下文,然后进行yield。待server回复后,触发epoll事件,然后resume。这里有一个超时机制,在设定时间内未能回包则返回false。
static PHP_METHOD(swoole_client_coro, recv)
{
...
swoole_client_coro_property *ccp = swoole_get_property(getThis(), 1);
if (ccp->iowait == SW_CLIENT_CORO_STATUS_DONE)
{
...
}
else if (ccp->iowait == SW_CLIENT_CORO_STATUS_WAIT && ccp->cid != COROG.current_coro->cid)
{
...
}
php_context *context = swoole_get_property(getThis(), 0);
if (timeout > 0)
{
php_swoole_check_timer((int) (timeout * 1000));
ccp->timer = SwooleG.timer.add(&SwooleG.timer, (int) (timeout * 1000), 0, context, client_coro_onTimeout);
}
ccp->iowait = SW_CLIENT_CORO_STATUS_WAIT;
coro_save(context);
ccp->cid = COROG.current_coro->cid;
coro_yield();
}
$client->close()这一步的操作主要是释放连接、释放内存,把对应的swoole_object置为NULL
static PHP_METHOD(swoole_client_coro, close)
{
swClient *cli = swoole_get_object(getThis());
...
//Connection error, or short tcp connection.
swoole_client_coro_property *ccp = swoole_get_property(getThis(), 1);
ccp->iowait = SW_CLIENT_CORO_STATUS_CLOSED;
cli->released = 1;
php_swoole_client_free(getThis(), cli TSRMLS_CC);
RETURN_TRUE;
}
以上关于swoole协程的介绍默认是在onReceive、onRequest等回调函数中使用协程客户端。除此之外,swoole还支持用户代码自行创建协程,在不支持协程的回调函数中,可以调用Coroutine::create自行创建协程,swoole2.1以上版本还采用了借鉴Go语言的协程语法糖,其底层调度原理和协程客户端是一致的。