由于水平有限,可能有些术语并不是那么准确。
PHP 8.1
的新特性Fiber
,可能让很多PHPer
误解了,以为Fiber
的出现可以解决PHP
生态的很多问题,如官方多线程方案的缺失、PHP-FPM
多进程阻塞模型的优化。但实际上Fiber
目前解决的问题并不多,只是在底层引入了用户栈空间切换的原语,并不能像进程和线程那样,在同步阻塞时,仍然可以在操作系统层面进行上下文切换,不至于让整个程序无法往后执行。
现在PHP
生态中,目前在这个方向可能走得最远的还是Swoole
,在PHP
内核层面将同步阻塞的函数进行hook
,让标准库的同步阻塞函数和类不再阻塞整个程序。而同类的如Workerman
、ReactPHP
、AMPHP
都无法做到,它们在使用所以造很多异步非阻塞的轮子。正如AMPHP
的官网上写的一句话“It’s important to avoid using blocking functions in concurrent code, such as sleep, usleep, fwrite, fread and other built-in functions doing I/O. We offer a great variety of non-blocking I/O implementations you can use instead.”。像Swoole
、Workerman
之类的库,一个很明显的特征就是使用了Event Loop
(事件循环)。什么是事件循环?事件,就是读、写、异常、时间等事件,循环就很好理解了,以下就是一个简单的事件循环事例:
<?php
// 创建套接字
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '127.0.0.1', 10101);
socket_listen($socket);
// 创建套接字数组
$sockets = [$socket];
while (true) {
$readSockets = $sockets;
$writeSockets = $exceptSockets = null;
// 选择可读、可写和异常套接字
socket_select($readSockets, $writeSockets, $exceptSockets, null);
// 处理可读套接字
foreach ($readSockets as $readSocket) {
if ($readSocket === $socket) {
// 接受新连接
$clientSocket = socket_accept($socket);
$sockets[] = $clientSocket;
echo "New client connected\n";
} else {
// 处理客户端请求
$data = socket_read($readSocket, 1024);
if ($data === false) {
// 客户端断开连接
$index = array_search($readSocket, $sockets);
unset($sockets[$index]);
socket_close($readSocket);
echo "Client disconnected\n";
} else if (!empty($data)) {
// 处理客户端数据
echo "Received data: $data\n";
// 这里可以添加你的业务逻辑代码
}
}
}
}
// 关闭套接字
socket_close($socket);
事件循环,也就是循环获取事件(通过select
、poll
、epoll
等机制),进行相对的业务操作,这就是Swoole
等库的原理。通过以上代码,我们可以发现,事件循环中,一旦业务代码出现同步阻塞操作,会让异步非阻塞退化成同步阻塞,导致代码无法将控制权交还给事件循环,后续的逻辑也就会延时执行,这也是很多异步库的通病。而Swoole
将标准库hook
之后,同步阻塞的操作变成异步非阻塞的,从而避免这种情况,这也是它很受欢迎的原因。而Go
语言的协程模型则更加优秀了,因为协程是包含在语言的设计中的,不像PHP
、Python
等语言,出现的时间太早,在语言层面并没有做相应的设计,导致“协程”功能都是缝缝补补,用起来也就需要多加注意。
Fiber
出现之初,Swoole
的作者就写文反对,且不管其它的,他说的话是有一定道理的,Fiber
需要一个统一的事件循环,最好是包含在PHP
内核中的,不然会造成各家自造轮子且这些轮子之间又无法共用的情况。当然,现在PHP
内核并没有事件循环,但社区也出现了Revolt
统一事件循环,这里先不展开说,核心原理跟上述代码差不多。另外PHP
的生态都是各种同步阻塞的库,Fiber
对目前生态作用不太大。
Fiber
跟Go
语言的协程相比,确实显得很简陋,它只提供了上下文切换的功能,而且是需要手动切换的,因此需要一些基础的设施才能让用户方便使用。以下代码简单封装了Fiber
:
<?php
class PHPCoroutine {
private array $fibers = [];
private array $params = [];
private array $output = [];
public function create(callable $callback, string|int $name, mixed $params): void
{
$this->params[$name] = $params;
$this->fibers[$name] = new Fiber($callback);
}
public function start(): void
{
while ($this->fibers) {
foreach ($this->fibers as $name => $fiber) {
try {
if (!$fiber->isStarted()) {
$fiber->start($this->params[$name]);
} else if ($fiber->isSuspended()) {
$fiber->resume();
} else if ($fiber->isTerminated()) {
$this->output[$name] = $fiber->getReturn();
unset($this->fibers[$name]);
}
} catch (Throwable $e) {
$this->output[$name] = $e;
unset($this->fibers[$name]);
}
}
}
}
public function output(): array
{
return $this->output;
}
}
$coroutine = new PHPCoroutine();
for ($i = 1; $i < 5; $i++) {
$coroutine->create(function ($params) {
if ($params < 3) {
Fiber::suspend();
}
// sleep(1);
return [$params, microtime(true)];
}, $i, $i);
}
$coroutine->start();
$output = $coroutine->output();
print_r($output);
试着将上述代码sleep(1)
前的注释去掉模拟同步阻塞任务,看看是什么效果。
下面将sleep(1)
换成真正的业务代码,对多个网站的网络情况进行测试,核心代码PHPCoroutine
不变:
<?php
function ping($url): false|string
{
$ping = proc_open('ping -c 4 '. $url, [
['pipe', 'r'],
['pipe', 'w'],
['file', '/tmp/error-output.txt', 'a'],
], $pipe);
while (proc_get_status($ping)['running']) {
// todo: 注释
Fiber::suspend();
}
return fread($pipe[1], 1024);
}
$coroutine = new PHPCoroutine();
$url = ['example.com', 'baidu.com', 'runoob.com'];
$start = microtime(true);
for ($i = 0; $i < 3; $i++) {
$coroutine->create(function ($params) {
$res = ping($params);
return [$params, microtime(true), $res];
}, $i, $url[$i]);
}
$coroutine->start();
$output = $coroutine->output();
echo $start, "\n";
print_r($output);
然后去除// todo: 注释
下面一行代码对比耗时。
说到这里,不得不再提一下Go
语言的协程,同样遇到同步阻塞操作,Go
的运行时可以利用多线程,让空闲的线程执行其它协程,而不需要等业务将控制权手动交还给事件循环,从而避免协程阻塞导致整个进程等待,而且调度不依赖于用户的行为。这倒有点像操作系统调度器的进化历史,协作式调度(PHP
)和抢占式调度(Go
),有点意思,之前看过这么一个说法所以,一个完备的协程库你可以把它理解为用户态的操作系统,而协程就是用户态操作系统里面的 “进程”。而Go
语言就实现了这么一个完备的协程库。