实现原理

Swoole-2.0基于setjmplongjmp实现,在进行协程切换时会自动保存Zend VM的内存状态(主要是EG全局内存和vm stack)。

  • setjmplongjmp主要是用于从ZendVMC堆栈跳回SwooleC回调函数
  • 协程的创建、切换、挂起、销毁全部为内存操作,消耗是非常低的

Swoole-4.0重构了协程内核,实现了C栈和PHP栈同时保存和切换,支持所有PHP语法。

示例代码

$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);

#1
$server->on('Request', function($request, $response) {
    $mysql = new Swoole\Coroutine\MySQL();
    #2
    $res = $mysql->connect([
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
    ]);
    #3
    if ($res == false) {
        $response->end("MySQL connect fail!");
        return;
    }
    $ret = $mysql->query('show tables', 2);
    $response->end("swoole response is ok, result=".var_export($ret, true));
});

$server->start();
  • 此程序仅启动了一个1个进程,就可以并发处理大量请求。
  • 程序的性能基本上与异步回调方式相同,但是代码完全是同步编写的

运行过程

  • 调用onRequest事件回调函数时,底层会调用C函数coro_create创建一个协程(#1位置),同时保存这个时间点的CPU寄存器状态和ZendVM stack信息。
  • 调用mysql->connect时发生IO操作,底层会调用C函数coro_save保存当前协程的状态,包括Zend VM上下文以及协程描述信息,并调用coro_yield让出程序控制权,当前的请求会挂起(#2位置)
  • 协程让出程序控制权后,会继续进入EventLoop处理其他事件,这时Swoole会继续去处理其他客户端发来的Request
  • IO事件完成后,MySQL连接成功或失败,底层调用C函数coro_resume恢复对应的协程,恢复ZendVM上下文,继续向下执行PHP代码(#3位置)
  • mysql->query的执行过程与mysql->connect一致,也会进行一次协程切换调度
  • 所有操作完成后,调用end方法返回结果,并销毁此协程

协程开销

相比普通的异步回调程序,协程多增加额外的内存占用。

  • Swoole2.0协程需要为每个并发保存zend stack栈内存并维护对应的虚拟机状态。如果程序并发很大可能会占用大量内存,取决于C函数、ZendVM 调用栈深度
  • 协程调度会增加额外的一些CPU开销

压力测试

  • 环境:Ubuntu16.04 + Core I5 4核 + 8G内存 PHP7.0.10
  • 脚本:ab -c 100 -n 10000 http://127.0.0.1:9501/

测试结果:

Server Software:        swoole-http-server
Server Hostname:        127.0.0.1
Server Port:            9501

Document Path:          /
Document Length:        348 bytes

Concurrency Level:      100
Time taken for tests:   0.883 seconds
Complete requests:      10000
Failed requests:        168
   (Connect: 0, Receive: 0, Length: 168, Exceptions: 0)
Total transferred:      4914560 bytes
HTML transferred:       3424728 bytes
Requests per second:    11323.69 [#/sec] (mean)
Time per request:       8.831 [ms] (mean)
Time per request:       0.088 [ms] (mean, across all concurrent requests)
Transfer rate:          5434.67 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       2
Processing:     0    9   9.6      6      96
Waiting:        0    9   9.6      6      96
Total:          0    9   9.6      6      96

Percentage of the requests served within a certain time (ms)
  50%      6
  66%      9
  75%     11
  80%     12
  90%     19
  95%     27
  98%     43
  99%     51
 100%     96 (longest request)

  • --影

    没错,协程基本都是setjmp、longjmp,汇编的jmp指令,更多用在抛异常

  • xhc

    coro_create coro_save coro_yield 和 core_resume 以后会不会开放给PHP使用?

  • 大眼刚哥

    一个设想:是不是可以往前走一步,比如位置#2,不挂起请求,继续往下执行,知道用到被赋值的变量$res时再挂起请求,直到有结果返回,这样就能并发的发起一些其他的请求处理。