Php 7.1 + Pecl-event + libevent - 在奇怪的情况下挂起

Posted

技术标签:

【中文标题】Php 7.1 + Pecl-event + libevent - 在奇怪的情况下挂起【英文标题】:Php 7.1 + Pecl-event + libevent - is hanging in weird case 【发布时间】:2017-11-29 10:53:35 【问题描述】:

基于this answer,我已切换到pecl-event 库。现在我有:

[root]# php -v
PHP 7.1.12 (cli) (built: Nov 22 2017 08:40:02) ( NTS ) Copyright (c) 1997-2017 The PHP Group Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologieswith Zend OPcache v7.1.12, Copyright (c) 1999-2017, by Zend Technologies 
[root]# php --info | grep event
/etc/php.d/event.ini event libevent2 headers version => 2.1.8-stable
[root]# pecl list
Installed packages, channel pecl.php.net:
=========================================
Package Version State
event   2.3.0   stable

下面的例子表现得很奇怪。如果从runme() 函数内部调用$loop->run(),它会起作用并调用回调。但是如果从runme() 外部调用$loop->run(),它就会挂起!

require_once __DIR__.'/../vendor/autoload.php';

$inner = count($argv) > 1;

$loop = new \React\EventLoop\ExtEventLoop();
//$loop = new \React\EventLoop\StreamSelectLoop();

runme($loop, $inner);

if (!$inner) 
    echo "Outer start\n";
    $loop->run();


function runme(\React\EventLoop\LoopInterface $loop, $inner)

    $contextOpts = [];
    $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
    $context = stream_context_create($contextOpts);
    $socket = stream_socket_client('tcp://127.0.0.1:3306', $errno, $errstr, 0, $flags, $context);
    stream_set_blocking($socket, 0);

    $loop->addWriteStream($socket, function ($socket) use ($loop) 
        echo "done  ".(false === stream_socket_get_name($socket, true) ? 'false' : 'true')."\n";
        $loop->removeWriteStream($socket);
    );

    if ($inner) 
        echo "Inner start\n";
        $loop->run();
    

    echo "Exit runme\n";

运行结果:

[root@vultr Scraper]# php ./tests/test.php --inner
Inner start
done  false
Exit runme
[root@vultr Scraper]# php ./tests/test.php 
Exit runme
Outer start
...............HANGING HERE...........

我错过了什么还是其中一个库/PHP 有问题?有人有运行php7.1 + react + libevent的经验吗?

更新:============================================== ========================

我使用最新的“react/socket”库“0.8.6”进行了测试。

require_once __DIR__.'/vendor/autoload.php';

$inner = count($argv) > 1;

$loop = new \React\EventLoop\ExtEventLoop();

$connector = new React\Socket\Connector($loop);

runme($loop, $connector, $inner);

if (!$inner) 
    echo "Outer start\n";
    $loop->run();


function runme(\React\EventLoop\LoopInterface $loop, React\Socket\Connector $connector, $inner)

    $connector->connect('tcp://127.0.0.1:3306')->
    then(function (\React\Socket\ConnectionInterface $conn) 
        echo ("Hello mysql!\n");
        $conn->close();
    ,function ($e) 
        echo ("Bye MySQL!\n");
    )->done();

    if ($inner) 
        echo "Inner start\n";
        $loop->run();
    

    echo "Exit runme\n";

它正常工作并返回:

$ php ./testMysql.php 
Exit runme
Outer start
Hello MySQL!
$ php ./testMysql.php  --inner
Inner start
Hello MySQL!
Exit runme

但是如果你进入 \React\Socket\TcpConnector::waitForStreamOnce() 并在新的 Promise 对象中删除 $canceller 函数,如下所示,它会再次挂起。看起来它可以在最新版本的 react 中工作,因为 socket 存储方式不明显,实际上类似于 v0.4.6 中的代码。

private function waitForStreamOnce($stream)
    
        $loop = $this->loop;

        return new Promise\Promise(function ($resolve, $reject) use ($loop, $stream) 
            $loop->addWriteStream($stream, function ($stream) use ($loop, $resolve, $reject) 
                $loop->removeWriteStream($stream);

                // The following hack looks like the only way to
                // detect connection refused errors with PHP's stream sockets.
                if (false === stream_socket_get_name($stream, true)) 
                    fclose($stream);

                    $reject(new \RuntimeException('Connection refused'));
                 else 
                    $resolve(new Connection($stream, $loop));
                
            );
        );
    



$ php ./testMysql.php  --inner
Inner start
.....HANGING
$ php ./testMysql.php 
Exit runme
Outer start
...HANGING

【问题讨论】:

【参考方案1】:

问题在于,当runme() 返回时,$socket 变量会被破坏(就像任何本地 PHP 变量一样!)。结果,在此套接字上打开的连接关闭

事件扩展尽最大努力防止内存泄漏,因此如果可能,它不会存储对用户变量的引用。特别是,所有接受socket resource(例如Event::__construct)的方法仅retrieve来自输入变量的底层数字文件描述符。 用户实际上有责任保持这些变量的活动

以下脚本通过将 $socket 移动到全局范围来解决此问题。

require_once 'vendor/autoload.php';

$inner = count($argv) > 1;
$loop = new \React\EventLoop\ExtEventLoop();
$socket = init_socket();

runme($loop, $socket, $inner);

if (!$inner) 
    echo "Outer start\n";
    $loop->run();


function init_socket()

    $contextOpts = [];
    $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
    $context = stream_context_create($contextOpts);
    $socket = stream_socket_client('tcp://test.local:80', $errno, $errstr, 0, $flags, $context);
    stream_set_blocking($socket, 0);
    return $socket;


function runme(\React\EventLoop\LoopInterface $loop, $socket, $inner)

    $loop->addWriteStream($socket, function ($socket) use ($loop) 
        echo "done  ".(false === stream_socket_get_name($socket, true) ? 'false' : 'true')."\n";
        $loop->removeWriteStream($socket);
    );

    if ($inner) 
        echo "Inner start\n";
        $loop->run();
    

    echo "Exit runme\n";

在实际应用程序中,您可能会将$socket 存储为类成员变量。

【讨论】:

正如我写的那样,这段代码是 react/socket-client 库 v0.4.6 的一部分,在 php5.6 中运行良好。所以看来我必须在我的应用程序中找到更新这个库。 @DmitryPismennyy,即使脚本在 PHP 5.6 上按预期工作,也只是因为认为不会破坏 $socket 的 GC 行为。我已经描述了问题的根源:必须保留$socket 变量(!);否则,连接将丢失,并且循环的行为将无法预测(它可能会挂起,或者可能会出现段错误......如果程序在关闭的文件描述符上运行,您会期望什么?)。这个想法也适用于 PHP 7。 我并不是指更新 PHP 5.6 中的工作代码。我的意思是在我的主要帖子中根据最新的反应库进行更新,乍一看意外地正常工作。 @DmitryPismennyy,您问题的第二部分看起来完全不同,因为您开始使用 React 连接器而不是显式套接字。此外,还不清楚底层流是如何创建的,如何传递给 Event 扩展的,以及它何时被销毁。为了找出这些问题的答案,需要对不同版本的 React 进行一些认真的调试。恐怕这稍微超出了 SO 的范围。我宁愿在他们的错误跟踪器中向 React 开发人员解决问题的第二部分。【参考方案2】:

嗨,这里是 ReactPHP 核心开发人员,刚刚查看了您的脚本,我可以在本地重现此内容,因此我将为此提交一个问题(即使这可能超出我们的范围)。

require_once __DIR__.'/../vendor/autoload.php';

$inner = count($argv) > 1;

$loop = new \React\EventLoop\ExtEventLoop();
//$loop = new \React\EventLoop\StreamSelectLoop();

runme($loop, $inner);

    $contextOpts = [];
    $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
    $context = stream_context_create($contextOpts);
    $socket = stream_socket_client('tcp://127.0.0.1:3306', $errno, $errstr, 0, $flags, $context);
    stream_set_blocking($socket, 0);

    $loop->addWriteStream($socket, function ($socket) use ($loop) 
        echo "done  ".(false === stream_socket_get_name($socket, true) ? 'false' : 'true')."\n";
        $loop->removeWriteStream($socket);
    );

    if ($inner) 
        echo "Inner start\n";
        $loop->run();
    

    echo "Exit runme\n";

但我建议您查看我们的socket component 以处理连接。

可能看起来像:

$loop = new \React\EventLoop\ExtEventLoop();
$connector = new React\Socket\Connector($loop);

$connector->connect('tcp://127.0.0.1:3306')->then(function (ConnectionInterface $conn) use ($loop) 
    $conn->write("Hello MySQL!\n");
);

$loop->run();

【讨论】:

感谢您的帮助。实际上,我已经从使用 react + react/mysql 库的应用程序中提取了上述代码,该库基于 \React\SocketClient\Connector()。应用程序适用于 php5.6 + libevent 2.0。但升级到 php7.1 后,我无法将可行的解决方案与事件库结合起来。 这是一个非常常见的异步编程问题,尤其是事件扩展。用户通常没有意识到扩展不保留对用于配置事件的输入变量的引用。 ($data 参数很特殊。)从用户的角度来看,只将套接字传递给某个事件构造函数然后忘记它可能看起来很方便。但是,此行为还需要用户在不再需要引用时释放它们。在我看来,后一种情况使异步编程更加困难。 正如我所写,这段代码是 react/socket-client 库 v0.4.6 的一部分,在 php5.6 中运行良好。所以看来我必须更新库。

以上是关于Php 7.1 + Pecl-event + libevent - 在奇怪的情况下挂起的主要内容,如果未能解决你的问题,请参考以下文章

php 7.1安装教程

CentOS 7.1编译安装PHP7

text 用于PHP的PHP 7.1 + nginx + MongoDB驱动程序

apache_conf 适用于PHP 7.0或PHP 7.1

apache_conf 适用于PHP 7.0或PHP 7.1

apache_conf 适用于PHP 7.0或PHP 7.1