随着 2024 年的结束,各位 PHP 开发者们所期待的 Swoole v6 正式发布了。作为我们技术进步的结晶,这一版本不仅整合了过去一年间社区的反馈与需求,还展现了开发团队的创新与努力。它标志着 ...
随着 2024 年的结束,各位 PHP 开发者们所期待的 Swoole v6 正式发布了。作为我们技术进步的结晶,这一版本不仅整合了过去一年间社区的反馈与需求,还展现了开发团队的创新与努力。它标志着 Swoole 在性能优化和功能拓展上的重大突破,为 PHP 开发者提供了更加强大的工具,助力我们在新的一年里开启更多无穷的可能。借此机会,让我们共同期待 Swoole v6 能够在实际应用中展现出超乎寻常的能力,推动我们的项目走向更加成功的未来!
Swoole v6 版本带来了令人振奋的 16 项全新功能,本文将详细介绍 Swoole v6 的各项新功能,包括它们的应用场景以及如何帮助我们更好地应对实际开发中的各种技术难题。
Swoole v6 最重要的更新就是增加了对多线程的支持。Swoole v6 的多线程是并非类似 Python Threading 的伪多线程,而是类似于 NodeJS Workers Thread 的技术实现,是真正的多线程。
得益于 PHP ZTS 强大的隔离性,与 Java、C++ 或 Golang 等语言提供的多线程相比,Swoole v6 的多线程使用起来更简单,更容易掌控,不会出现危险的数据竞争。
可以在 new Thread 的构造方法中传入变长参数数组,为新的线程传递参数。
use Swoole\Thread;
// 如果是主线程,$args 为空,如果是子线程,$args 不为空
$args = Thread::getArguments();
$c = 4;
if (empty($args)) {
# 主线程
for ($i = 0; $i < $c; $i++) {
$threads[] = new Thread(__FILE__, $i);
}
for ($i = 0; $i < $c; $i++) {
$threads[$i]->join();
}
} else {
# 子线程
echo "Thread #" . $args[0] . "\n";
while (1) {
sleep(1);
file_get_contents('https://www.baidu.com/');
}
}
在 Swoole v6 线程中依然可以使用协程 API 实现异步非阻塞 IO。
use Swoole\Thread;
$args = Thread::getArguments();
$c = 4;
if (empty($args)) {
# main thread
for ($i = 0; $i < $c; $i++) {
$threads[] = new Thread(__FILE__, $i);
}
for ($i = 0; $i < $c; $i++) {
$threads[$i]->join();
}
} else {
# child thread x 4
echo "Thread #" . $args[0] . "\n";
Co\run(function() {
while (1) {
sleep(1);
Co\go(function () {
file_get_contents('https://www.baidu.com/');
});
}
});
}
与多进程相比,线程最大的优势是共享内存堆栈。在多线程环境下,我们可以实现更加灵活、强大的并发数据容器。Swoole v6 提供了 3 种数据容器,包括:ArrayList、Map、Queue。
ArrayList、Map 实现了 ArrayAccess 接口,可以直接当做 PHP 数组来使用。Queue 是一个自带线程条件变量和锁的先进先出队列结构,可以作为线程间消息通信的容器。除了数字、字符串等内容外,还可以将 Stream、CoSocket 或 Socket 等资源直接保存到数据容器中。ArrayList、Map、Queue 还支持多维嵌套结构,例如 ArrayList 可以作为 Map 的一个元素。
数据容器均是线程安全的,对于容器的读写操作底层会自动使用 Mutex 互斥锁进行加锁,无需担心出现数据一致性问题。
ArrayListArrayList 是一种顺序容器,类似于 PHP 的数字索引数组。
use Swoole\Thread;
use Swoole\Thread\ArrayList;
$list = new ArrayList();
# 追加元素
$list[] = time();
$list[] = 99999;
$list[2] = 'test';
# 获取长度
count($list);
# 删除一个元素,这会引起批量数据前移,填补空位
unset($list[1]);
# 赋值
$list[0] = 0;
# 抛出 out of range 异常,错误的赋值
$list[1000] = 0;
MapMap 是一种关联容器,类似于 PHP 的关联索引数组。
use Swoole\Thread;
use Swoole\Thread\Map;
$map = new Map;
# 写入
$map[time()] = 'value';
$map['hello'] = 3.1415926;
# 读取
echo $map['hello'];
# 删除
unset($map['hello']);
# 获取长度
count($map);
Queueuse Swoole\Thread;
use Swoole\Thread\Queue;
$args = Thread::getArguments();
$c = 4;
$n = 128;
if (empty($args)) {
$threads = [];
$queue = new Queue;
for ($i = 0; $i < $c; $i++) {
$threads[] = new Thread(__FILE__, $i, $queue);
}
while ($n--) {
$queue->push(base64_encode(random_bytes(16)), Queue::NOTIFY_ONE);
usleep(random_int(10000, 100000));
}
$n = 4;
while ($n--) {
$queue->push('', Queue::NOTIFY_ONE);
}
for ($i = 0; $i < $c; $i++) {
$threads[$i]->join();
}
var_dump($queue->count());
} else {
$queue = $args[1];
while (1) {
$job = $queue->pop(-1);
if (!$job) {
break;
}
var_dump($job);
}
}
Swoole v6 除了 Queue 进行一些线程通信和同步之外,还提供了多种线程同步工具来管理多线程,包括: - Swoole\Thread\Lock:可以在多线程之间访问临界资源时的指令进行互斥 - Swoole\Thread\Atomic:原子计数器,实现各种数字的 CAS 原子操作 - Swoole\Thread\Barrier:实现发令***功能,可以让多个线程在资源对齐后并行执行
Thread::getId():获取当前线程的 ID
Thread::getNativeId():获取操作系统为线程分配的唯一 ID
Thread::getArguments():获取父线程传递给子线程的参数列表
Thread::join():等待子线程退出,请注意 $thread 对象销毁时会自动执行 join() ,这可能会导致进程阻塞
Thread::joinable():检测子线程是否已退出
Thread::detach():使子线程独立运行,不再需要 Thread::join()
Thread::getPriority():获取线程的调度优先级信息
Thread::setPriority():设置线程的调度优先级信息
Thread::getAffinity():获取线程的 CPU 亲缘性
Thread::setAffinity():设置线程的 CPU 亲缘性,可以执行线程在哪些 CPU 核心上运行
Thread::getExitStatus():获取子线程调用 exit() 函数退出时设置的状态码
Thread::setName():为线程设置独特的线程名称,以便于 ps 或 gdb 等工具更好地追踪和分析
Swoole v6 的服务器端模块也适配了多线程,提供了 SWOOLE_THREAD 模式。在此模式下所有的 Event Worker、Task Worker 以及 User Worker 将改为创建一个线程来执行,而不是进程。在工作线程之间可以传递一些 ArrayList 和 Map 等线程资源实现数据资源共享。
use Swoole\Process;
use Swoole\Thread;
use Swoole\Http\Server;
$http = new Server("0.0.0.0", 9503, SWOOLE_THREAD);
$http->set([
'worker_num' => 2,
'task_worker_num' => 3,
'bootstrap' => __FILE__,
// 通过 init_arguments 实现线程间的数据共享
'init_arguments' => function () use ($http) {
$map = new Swoole\Thread\Map;
return [$map];
}
]);
$http->on('Request', function ($req, $resp) use ($http) {
$resp->end('hello world');
});
$http->on('pipeMessage', function ($http, $srcWorkerId, $msg) {
echo "[worker#" . $http->getWorkerId() . "]\treceived pipe message[$msg] from " . $srcWorkerId . "\n";
});
$http->addProcess(new Process(function () {
echo "user process, id=" . Thread::getId();
sleep(2000);
}));
$http->on('Task', function ($server, $taskId, $srcWorkerId, $data) {
var_dump($taskId, $srcWorkerId, $data);
return ['result' => uniqid()];
});
$http->on('Finish', function ($server, $taskId, $data) {
var_dump($taskId, $data);
});
$http->on('WorkerStart', function ($serv, $wid) {
// 通过Swoole\Thread::getArguments()获取配置中的init_arguments传递的共享数据
var_dump(Thread::getArguments(), $wid);
});
$http->on('WorkerStop', function ($serv, $wid) {
var_dump('stop: T' . Thread::getId());
});
$http->start();
io_uring 是 2019 年 Linux 5.1 内核首次引入高性能、革命性的异步 I/O 框架,能显著加速 I/O 密集型应用的性能。 Swoole v6 引入 io_uring 使得 Swoole 的异步文件读写性能得到了大幅提升。PHP 应用层不需要任何更改即可使用。现在基于 Swoole v6 不仅可编写高性能的网络服务器,也可以实现高性能的文件存储服务器。这必将进一步拓宽 PHP 编程语言的应用范围。
direct I/O 1KB 随机读(绕过 Page Cache)backend | IOPS | context switches | IOPS ±% vs io_uring |
|---|---|---|---|
sync | 814,000 | 27,625,004 | -42.6% |
thread pool | 433,000 | 64,112,335 | -69.4% |
io_uring | 1,417,000 | 11,309,574 | - |
buffered I/O 1KB 随机读(命中 Page Cache)backend | IOPS | context switches | IOPS ±% vs io_uring |
|---|---|---|---|
sync | 4,906,000 | 105,797 | -2.3% |
thread pool | 1,070,000 | 114,791,187 | -78.7% |
io_uring | 5,024,000 | 106,683 | - |
在之前的版本中,我们提供的 Cookie API 风格与 PHP 的 setcookie() 函数完全一致,随着互联网的发展,Cookie 的选项越来越多,导致此函数的参数长达十几项,非常难以维护。Swoole v6 提供了全新的 Cookie API,使用了更加现代化的面向对象风格来简化 Cookie 设置。
$server->on('request', function (Request $request, Response $response) {
$cookie = new Swoole\Http\Cookie();
$cookie->withName('key1')
->withValue('val1')
->withExpires(time() + 84600)
->withPath('/')
->withDomain('id.test.com')
->withSecure(true)
->withHttpOnly(true)
->withSameSite('None')
->withPriority('High')
->withPartitioned(true);
$response->setCookie($cookie);
$response->end("<h1>Hello Swoole. #" . rand(1000, 9999) . "</h1>");
});
Swoole v6 提供了全新协程锁实现,使用协程锁可以更加方便地实现协程之间的互斥保护逻辑。协程锁还允许将自身设置为共享内存模式,实现跨进程的互斥。
use Swoole\Coroutine\Lock;
use Swoole\Coroutine\System;
Co\run(function () {
$lock = new Lock(false);
Assert::eq($lock->trylock(), true);
go(function () use ($lock) {
Assert::eq($lock->trylock(), false);
$s = microtime(true);
Assert::eq($lock->lock(), true);
Assert::assert(microtime(true) - $s >= 0.05);
echo "co2 end\n";
});
System::sleep(0.05);
Assert::eq($lock->unlock(), true);
echo "co1 end\n";
});
echo "DONE\n";
以上代码中处于 lock() 和 unlock() 函数之间的 PHP 代码是互斥的,不会并发执行。
Swoole v6 恢复了早期版本提供的异步客户端,某些场景下我们希望有一个逻辑直接运行在异步的事件循环之上,不创建协程环境,就可以使用此异步客户端。
$cli = new Swoole\Async\Client(SWOOLE_SOCK_TCP);
$client->on("connect", function(Swoole\Async\Client $client) {
Assert::true($client->isConnected());
$client->send(RandStr::gen(1024, RandStr::ALL));
});
$client->on("receive", function(Swoole\Async\Client $client, string $data){
$recv_len = strlen($data);
$client->send(RandStr::gen(1024, RandStr::ALL));
$client->close();
Assert::false($client->isConnected());
});
$client->on("error", function(Swoole\Async\Client $client) {
echo "error";
});
$client->on("close", function(Swoole\Async\Client $client) {
echo "close";
});
$client->connect("127.0.0.1", 9501, 0.2);
PHP 8.4Swoole v6 版本对新的 PHP 8.4 进行了适配。
Boost Context 汇编Swoole v6 版本使用了最新的 boost context 1.84 汇编代码,实现底层的协程上下文切换。性能和稳定性上得到了提升,并且还支持了龙芯等全新的 CPU 类型。
zstd 压缩格式Zstd 全称叫 Zstandard,是一个提供高压缩比的快速压缩算法 。Zstd 是 Facebook 于 2016 年发布的,采用了有限状态熵(Finite State Entropy,缩写为 FSE)编码器。该编码器是由 Jarek Duda 基于 ANS 理论开发的一种新型熵编码器,提供了非常强大的压缩速度 / 压缩率的折中方案(事实上也的确做到了 “鱼” 和 “熊掌” 兼得)。Zstd 在其最大压缩级别上提供的压缩比接近 lzma、lzham 和 ppmx,并且性能优于 lza 或 bzip2。Zstandard 达到了 Pareto frontier(资源分配最佳的理想状态),因为它解压缩速度快于任何其他当前可用的算法,但压缩比类似或更好。
对于小数据,它还特别提供一个载入预置词典的方法优化速度,词典可以通过对目标数据进行训练从而生成。
相比常见的 gz 或 brotli,zstd 压缩算法性能有明显的提升。
Swoole v6 版本支持了 HTTP2 分段发送。现在使用 HTTP2 协议时也可以支持 streaming 模式了。
$http = new Swoole\Http\Server("0.0.0.0", 9501);
$http->set([
'open_http2_protocol' => 1,
]);
$http->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
$n = 5;
while ($n--) {
$response->write("hello world, #$n <br />\n");
Co\System::sleep(1);
}
$response->end("hello world");
});
$http->start();
nghttp -v http://localhost:9501
[ 0.001] Connected
[ 0.001] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
(niv=2)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[ 0.001] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
(dep_stream_id=0, weight=201, exclusive=0)
[ 0.001] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
(dep_stream_id=0, weight=101, exclusive=0)
[ 0.001] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
(dep_stream_id=0, weight=1, exclusive=0)
[ 0.001] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
(dep_stream_id=7, weight=1, exclusive=0)
[ 0.001] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
(dep_stream_id=3, weight=1, exclusive=0)
[ 0.001] send HEADERS frame <length=38, flags=0x25, stream_id=13>
; END_STREAM | END_HEADERS | PRIORITY
(padlen=0, dep_stream_id=11, weight=16, exclusive=0)
; Open new stream
:method: GET
:path: /
:scheme: http
:authority: localhost:9501
accept: */*
accept-encoding: gzip, deflate
user-agent: nghttp2/1.43.0
[ 0.001] recv SETTINGS frame <length=30, flags=0x00, stream_id=0>
(niv=5)
[SETTINGS_HEADER_TABLE_SIZE(0x01):4096]
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):4294967295]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[SETTINGS_MAX_FRAME_SIZE(0x05):16384]
[SETTINGS_MAX_HEADER_LIST_SIZE(0x06):4294967295]
[ 0.002] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
; ACK
(niv=0)
[ 0.003] recv (stream_id=13) :status: 200
[ 0.003] recv (stream_id=13) server: swoole-http-server
[ 0.003] recv (stream_id=13) date: Tue, 31 Dec 2024 05:04:04 GMT
[ 0.003] recv (stream_id=13) content-type: text/html
[ 0.003] recv HEADERS frame <length=49, flags=0x04, stream_id=13>
; END_HEADERS
(padlen=0)
; First response header
hello world, #4 <br />
[ 0.003] recv DATA frame <length=23, flags=0x00, stream_id=13>
hello world, #3 <br />
[ 1.004] recv DATA frame <length=23, flags=0x00, stream_id=13>
hello world, #2 <br />
[ 2.005] recv DATA frame <length=23, flags=0x00, stream_id=13>
hello world, #1 <br />
[ 3.006] recv DATA frame <length=23, flags=0x00, stream_id=13>
hello world, #0 <br />
[ 4.007] recv DATA frame <length=23, flags=0x00, stream_id=13>
hello world[ 5.008] recv DATA frame <length=11, flags=0x01, stream_id=13>
; END_STREAM
[ 5.008] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
(last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])System::waitSignal() 可同时监听多个信号# 信号触发后,将返回信号的数值
$signal = System::waitSignal([SIGUSR1, SIGUSR2, SIGIO]);
HTTP 头之前的版本中收到重复的 HTTP 头之后,将丢弃之前的。新版本将变为数组,保留所有收到的 HTTP 头。
$client = new Swoole\Coroutine\Http\Client('127.0.0.1', 9501);
Assert::true($client->get('/'));
Assert::eq($client->headers['values-1'], ['hello', 'swoole', $uuid]);
Assert::eq($client->headers['values-2'], ['hello', $uuid]);
Swoole\Redis\Server 现在可以向客户端发送嵌套结构了。
$server->setHandler('GET', function ($fd, $data) use ($server) {
$key = $data[0];
if ($key == 'map') {
$out = Server::format(Server::MAP, [
'uuid' => UUID,
'list' => [1, NUMBER, UUID],
'number' => NUMBER,
]);
} elseif ($key == 'set') {
$out = Server::format(Server::SET, [
UUID,
['number' => NUMBER, 'uuid' => UUID],
NUMBER,
]);
} else {
$out = Server::format(Server::ERROR, 'bad key');
}
$server->send($fd, $out);
});
CoSocket::getOption() 可获取 TCP_INFO某些场景下我们希望获取到 TCP 连接的 QoS 信息,例如平均延时、丢包率等就可以在 TCP_INFO 信息中寻找。
$content = http_get_with_co_socket('www.baidu.com', function ($cli, $content){
$info = $cli->getOption(SOL_TCP, TCP_INFO);
Assert::greaterThan($info['rcv_space'], 0);
Assert::greaterThan($info['rto'], 0);
Assert::greaterThan($info['rtt'], 0);
Assert::greaterThan($info['snd_mss'], 0);
Assert::greaterThan($info['rcv_mss'], 0);
echo "DONE\n";
});
Assert::assert(strpos($content, 'map.baidu.com') !== false);
Process::getAffinity()可获取当前进程的 CPU 亲缘性设置。
Process\Pool 事件回调和属性onStart:进程池启动时回调
onShutdown:进程池终止时回调
onWorkerExit:异步模式的工作进程即将退出时回调
workerPid:当前工作进程的 PID
workerId:当前工作进程的 ID
running:进程池是否处于运行状态,在收到 SIGTERM 信号后将切换为 false
workerRunning:当前工作进程是否处于运行状态,在收到 SIGTERM 信号后将切换为 false
在经历了对 Swoole v6 新功能的深入探讨后,我们可以清晰地看到,这一版本不仅是功能的简单叠加,更是对开发者需求的深度洞察与响应。随着这些创新功能的引入,我们的开发流程将变得更加高效,应用性能也将得到显著提升。这无疑为我们在未来项目中解决复杂问题提供了更为坚实的基础。
展望未来,Swoole v6 诚如一把利剑,将继续助力开发者应对日益复杂的技术挑战。我们期待着开发者们能够充分利用这些新特性,推动项目的不断创新与进步。同时,Swoole 社区也将一如既往地支持大家,为大家提供必要的帮助与资源,构建一个更加繁荣的技术生态。
最后,感谢每一位参与 Swoole 发展的开发者和用户。正是因为有了大家的共同努力与支持,Swoole 才能不断成长与进步。在新的一年里,让我们携手并进,共同开创更加辉煌的未来!