异步回调程序内存管理

异步回调程序与同步阻塞程序的内存管理方式不同,异步程序是基于回调链引用计数实现内存的管理。本文会用一个最简单的实例讲解异步程序的内存管理。

实例程序

$serv = new Swoole\Http\Server("127.0.0.1", 9502);

$serv->on('Request', function($request, $response) {
    $cli = new Swoole\Http\Client('127.0.0.1', 80);
    $cli->post('/dump.php', array("key" => 'value'), function ($cli) use ($request, $response) {
        $response->end("<h1>{$cli->body}</h1>");
        $cli->close();
    });
});

$serv->start();

onRequest

  • 请求到来这时会触发onRequest回调函数,可以得到$request$response对象
  • onRequest回调函数中,创建了一个Http\Client,并发起一次POST请求
  • 然后onRequest函数结束并返回

这时按照正常的PHP函数调用流程,$request$response对象会被销毁。但在上述程序中,$request$response对象被使用了use语法,绑定到了匿名函数上,因此这2个对象的引用计数会被加1onRequest函数返回时就不会真正销毁这2个对象了。

引用链依赖

request/response -> post(Closure 回调函数) -> $cli(HttpClient对象) -> post($cli->connect)

$cli对象,是在onRequest函数创建的局部变量,按照正常逻辑$cli对象在onRequest函数退出时也应该被销毁。但Swoole底层有一个特殊的逻辑,所有异步客户端对象在发起连接时底层会自动增加一次引用计数,在连接关闭时减少一次引用计数,因此$cli对象也不会销毁,POST请求中的匿名函数对象也不会销毁。

Http响应

  • 创建的$cli对象,接收到来自服务器端的响应,或者连接超时、响应超时,这时会回调指定的匿名函数,调用end向客户端发送响应
  • 回调函数中调用了$cli->close这时切断连接,$cli的引用计数减一。这时匿名函数退出底层会自动销毁$cli$request$response 3个对象

引用链解除

cli->close -> Closure 销毁 -> $cli 销毁 -> request/response 销毁

多层嵌套

如果Http\Client的回调函数中调用了其他的异步客户端,如Swoole\Redis,对象会继续传读引用,形成一个异步调用链。当调用链的最后一个对象销毁时会向着调用链头部逐个递减引用计数,最终销毁对象。

$serv = new Swoole\Http\Server("127.0.0.1", 9502);

$serv->on('Request', function($request, $response) {
    $cli = new Swoole\Http\Client('127.0.0.1', 80);
    //发起连接,$cli 引用计数增加
    $cli->post('/dump.php', array("key" => 'value'), function ($cli) use ($request, $response) {
        $redis = new Swoole\Redis;
        //发起连接,$redis 引用计数增加
        $redis->connect('127.0.0.1', 6379, function ($redis, $result) use ($request, $response, $cli) {
            $redis->get('test_key', function ($redis, $result) use ($request, $response, $cli) {
                $response->end("<h1>{$result}</h1>");
                //关闭连接,$cli 引用计数减少
                $cli->close();
                //关闭连接,$redis 引用计数减少
                $redis->close();
            });
        });
    });
});

$serv->start();
  • 这里$response$request对象被POST匿名函数、Redis->connect匿名函数、Redis->get匿名函数引用,因此需要等到这3个函数执行后,引用计数减少为0,才会真正的销毁
  • $cli$redis对象在发起TCP连接时,会被Swoole底层增加引用计数。只有$cli->close()$redis->close被调用,或者远端服务器关闭连接,触发$cli->onClose$redis->onClose$cli$redis2个对象的,引用计数才会减少,函数退出时会销毁
  • POST匿名函数、Redis->connect匿名函数、Redis->get匿名函数,3个对象依附于$cli$redis对象,当$cli$redis对象销毁时,这3个对象也会被销毁
  • POST匿名函数、Redis->connect匿名函数、Redis->get匿名函数,匿名函数销毁时通过use语法引用的$response$request对象也会销毁