聊聊Swoole2.0协程
Swoole 2.0正式版发布了。2.0版本最大的更新是增加了对协程(Coroutine)的支持。正式版已同时支持PHP5和PHP7。基于Swoole2.0协程PHP开发者可以已同步的方式编写代码,底层自动进行协程调度,转变为异步IO。解决了传统异步编程嵌套回调的问题。
目前Swoole底层内置的协程客户端组件包括:udpclient、tcpclient、httpclient、redisclient、mysqlclient,基本涵盖了开发者常用的几种通信协议。协程组件只能在服务器的onConnect、onRequest、onReceive、onMessage 回调函数中使用。
注意,Swoole 2.0.5以前的版本还是灰度测试版本,可能会存在问题。 beta是因为协程是全新的版本。
协程的使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
/** 只有在Server中才能使用协程。包括 http server,websocket server 和 server。 */ $server = new Swoole\Http\Server( '127.0.0.1' , 9501); /** 触发on request事件时,SWOOLE会开辟一个协程栈,对协程栈进行初始化 */ $server ->on( 'Request' , function ( $request , $response ) { $tcp_cli = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP); /** client在调用connect函数后,SWOOLE会将PHP上下文信息保存到当前栈内 然后将协程挂起,待确认连接成功后,触发epoll事件,然后协程切换 恢复PHP上下文信息,返回结果,继续执行PHP代码 */ if ( $tcp_cli ->connect( '127.0.0.1' , 9906) === false) { $response -> end ( "connect server failed." ); return ; } $tcp_cli ->send( 'test for the coro' ); /** client在调用recv函数后,SWOOLE会将PHP上下文信息保存到当前栈内 然后将协程挂起待后端svr回包,触发epoll事件,然后协程切换 恢复PHP上下文信息,返回结果,继续执行PHP代码 如果后端在设定的超时时间内,未能回包,返回false client的errCode定为110 */ $ret = $tcp_cli ->recv(100); $tcp_cli ->close(); if ( $ret ) { $response -> end ( " swoole response is ok" ); } else { $response -> end ( " recv failed error : {$tcp_cli->errCode}" ); } }); $server ->start(); |
协程的执行流程
[sequence]
主进程->回调函数: 嘿,有数据到了,麻烦你处理下
Note right of 回调函数: 遇到异步IO
回调函数-->主进程: 已保存状态,你执行其他任务吧
Note left of 主进程: 去执行其他任务
主进程->回调函数: 发送的数据,有消息返回啦,你处理下
[/sequence]
咱们就以上面的示例代码为例,说一说协程的执行流程。
Http Server监听9051端口。当有相关事件发生时,如有数据到达,就会执行绑定到Request上的回调函数。在执行回调函数之前,会创建一个协程。这时,会保存CPU寄存器的状态和ZendVM Stack信息。
在回调函数执行过程中,如果遇到IO操作,如$tcp_cli->connect(
,就会保存当前的状态,并让出CPU使用权。当前请求执行被挂起。
让出CPU使用权后,CPU就可以用于处理其他事件。如处理其他客户端的Request请求。
当被挂起的请求,又有新的事件发生,如上面$tcp_cli->connect()
的数据已经返回。这时,会使用挂起前保存的状态信息恢复,然后继续执行回调函数。
如果在执行过程中,再次遇到IO操作,会继续执行保存状态和让出CPU使用权。
协程的意义
这些IO操作都是非阻塞的,即发送请求和获取数据分为两步。当请求发送完毕后,就会进行状态保存和让出CPU使用权。在等待请求数据返的这段时间,CPU可以执行一些其他程序。这样就可以充分利用CPU。
协程的实现
Swoole的协程是基于 setjmp 、 longjmp 实现的。Swoole为每个协程都分配了空间,用于保存协程切换时的状态信息。进行协程切换时会自动保存Zend VM的内存状态(主要是EG全局内存和vm stack)。当回调函数执行完毕后,会自动销毁分配的空间。
创建协程
什么时候会创建协程?在Server的onConnect、onRequest、onReceive、onMessage 回调函数被执行前会创建一个协程。
协程创建的方法是coro_create
。相关源码可以查看swoole_coroutine.c
文件。
coro_create
方法中主要进行了如下操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
int sw_coro_create(zend_fcall_info_cache *fci_cache, zval **argv, int argc, zval **retval, void *post_callback, void * params) { // 为回调函数的执行做一些准备工作 ....... COROG.require = 1; // 使用setjmp开启一个协程 if (! setjmp (*swReactorCheckPoint)) { // setjmp第一次调用会进入此代码分支,执行回调函数 zend_execute_ex(execute_data TSRMLS_CC); ...... // 执行完毕后,关闭协程 coro_close(TSRMLS_C); ...... coro_status = CORO_END; } else { /** 如果执行longjump,会调到上面的setjmp(*swReactorCheckPoint)行。 但是,setjmp的返回值为非0。因此,longjump后,会进入此代码分支。 让出CPU执行权。 */ coro_status = CORO_YIELD; } COROG.require = 0; return coro_status; } |
协程让出CPU执行权yield
什么时候会让出CPU执行权?当回调函数中遇到异步IO的时候,会让出CPU执行权。如,代码中的connect操作。下面,我们就以connect操作为例,看看让出CPU执行权时都做了那些操作。
connect的相关代码在swoole_coroutine.c
文件中。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
static PHP_METHOD(swoole_client_coro, connect) { long port = 0, sock_flag = 0; ...... //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); // 让出CPU使用权 coro_yield(); } |
保存协程信息
所谓的协程信息主要就是当前的上下文执行信息。coro_save
方法在swoole_coroutine.c
文件中。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
sw_inline php_context *sw_coro_save(zval *return_value, php_context *sw_current_context) { // 下面的代码主要是把当前的执行状态保存到之前获取的内存空间中 zend_execute_data *current = EG(current_execute_data); if (ZEND_CALL_INFO(current) & ZEND_CALL_RELEASE_THIS) { zval_ptr_dtor(&(current->This)); } zend_vm_stack_free_args(EG(current_execute_data)); zend_vm_stack_free_call_frame(EG(current_execute_data)); strncpy (SWCC(uid), COROG.uid, 20); SWCC(current_coro_return_value_ptr) = return_value; SWCC(current_execute_data) = EG(current_execute_data)->prev_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; } |
让出CPU执行权
coro_yield
方法的作用是让出CPU执行权。代码在swoole_coroutine.c
文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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 // 跳转到coro_create方法中setjmp代码行。 longjmp (*swReactorCheckPoint, 1); } |
在这个方法中主要进行了还原栈信息和longjump操作。
COROG.origin_vm_stack 这些栈信息的初始化在coro_init
方法中。记录了协程执行前的状态。
恢复协程
当异步IO有数据返回后,会进行协程恢复。协程恢复的方法是coro_resume
。在swoole_coroutine.c
文件中。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
int sw_coro_resume(php_context *sw_current_context, zval *retval, zval *coro_retval) { // 使用之前保存的协程信息恢复执行上下文环境。 EG(vm_stack) = SWCC(current_vm_stack); .... int coro_status; // 设置跳转点,方便在执行过程中再遇到异步IO操作,进行跳转。 if (! setjmp (*swReactorCheckPoint)) { //coro exit // 继续执行回调函数 zend_execute_ex(sw_current_context->current_execute_data TSRMLS_CC); coro_close(TSRMLS_C); coro_status = CORO_END; } else { //coro yield coro_status = CORO_YIELD; } 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; } |
可见,创建协程和恢复协程的整体代码结构差不多。
结束协程
当回到函数执行完毕后,会结束协程。
coro_close
方法用于结束协程。源码在swoole_coroutine.c
文件中。
1
2
3
4
5
6
7
8
9
10
11
12
|
sw_inline void coro_close(TSRMLS_D) { // 释放为协程而申请的相关资源 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; swTrace( "closing coro and %d remained. usage size: %zu. malloc size: %zu" , COROG.coro_num, zend_memory_usage(0), zend_memory_usage(1)); } |
原文链接:Swoole2.0协程的使用和源码解读,转载请注明来源!