随着 2024 年的结束,各位PHP开发者们所期待的Swoole v6正式发布了。作为我们技术进步的结晶,这一版本不仅整合了过去一年间社区的反馈与需求,还展现了开发团队的创新与努力。它标志着Swoole在性能优化和功能拓展上的重大突破,为PHP开发者提供了更加强大的工具,助力我们在新的一年里开启更多无穷的可能。借此机会,让我们共同期待Swoole v6能够在实际应用中展现出超乎寻常的能力,推动我们的项目走向更加成功的未来!
Swoole v6版本带来了令人振奋的16项全新功能,本文将详细介绍Swoole v6的各项新功能,包括它们的应用场景以及如何帮助我们更好地应对实际开发中的各种技术难题。
1. 多线程支持
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互斥锁进行加锁,无需担心出现数据一致性问题。
ArrayList
ArrayList 是一种顺序容器,类似于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;
Map
Map 是一种关联容器,类似于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);
Queue
use 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等工具更好地追踪和分析
2. 多线程服务器
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('stransform: translateY( T' . Thread::getId());
});
$http->start();
3. 增加 IO-Uring 支持,读写磁盘文件性能大幅提升
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)
backendIOPScontext switchesIOPS ±% vs io_uringsync814,00027,625,004-42.6%thread pool433,00064,112,335-69.4%io_uring1,417,00011,309,574-
场景二:buffered I/O 1KB 随机读(命中 Page Cache)
backendIOPScontext switchesIOPS ±% vs io_uringsync4,906,000105,797-2.3%thread pool1,070,000114,791,187-78.7%io_uring5,024,000106,683-
4. 全新的 Cookie API
在之前的版本中,我们提供的 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("Hello Swoole. #" . rand(1000, 9999) . "
");
});
5. 协程锁
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);
7. 支持 PHP 8.4
Swoole v6 版本对新的PHP 8.4进行了适配。
8. 更新到最新版本的 Boost Context 汇编
Swoole v6 版本使用了最新的 boost context 1.84 汇编代码,实现底层的协程上下文切换。性能和稳定性上得到了提升,并且还支持了龙芯等全新的CPU类型。
9. 支持 zstd 压缩格式
Zstd 全称叫 Zstandard,是一个提供高压缩比的快速压缩算法 。Zstd是Facebook于2016年发布的,采用了有限状态熵(Finite State Entropy,缩写为FSE)编码器。该编码器是由Jarek Duda 基于ANS理论开发的一种新型熵编码器,提供了非常强大的压缩速度/压缩率的折中方案(事实上也的确做到了“鱼”和“熊掌”兼得)。Zstd 在其最大压缩级别上提供的压缩比接近 lzma、lzham 和 ppmx,并且性能优于 lza 或 bzip2。Zstandard 达到了 Pareto frontier(资源分配最佳的理想状态),因为它解压缩速度快于任何其他当前可用的算法,但压缩比类似或更好。
对于小数据,它还特别提供一个载入预置词典的方法优化速度,词典可以通过对目标数据进行训练从而生成。
image
相比常见的gz或brotli,zstd压缩算法性能有明显的提升。
10. 支持 HTTP2 分段发送
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
\n");
Co\System::sleep(1);
}
$response->end("hello world");
});
$http->start();
运行结果
nghttp -v http://localhost:9501
[ 0.001] Connected
[ 0.001] send SETTINGS frame
(niv=2)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[ 0.001] send PRIORITY frame
(dep_stream_id=0, weight=201, exclusive=0)
[ 0.001] send PRIORITY frame
(dep_stream_id=0, weight=101, exclusive=0)
[ 0.001] send PRIORITY frame
(dep_stream_id=0, weight=1, exclusive=0)
[ 0.001] send PRIORITY frame
(dep_stream_id=7, weight=1, exclusive=0)
[ 0.001] send PRIORITY frame
(dep_stream_id=3, weight=1, exclusive=0)
[ 0.001] send HEADERS frame
; 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
(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
; 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
; END_HEADERS
(padlen=0)
; First response header
hello world, #4
[ 0.003] recv DATA frame
hello world, #3
[ 1.004] recv DATA frame
hello world, #2
[ 2.005] recv DATA frame
hello world, #1
[ 3.006] recv DATA frame
hello world, #0
[ 4.007] recv DATA frame
hello world[ 5.008] recv DATA frame
; END_STREAM
[ 5.008] send GOAWAY frame
(last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])
11. System::waitSignal() 可同时监听多个信号
# 信号触发后,将返回信号的数值
$signal = System::waitSignal([SIGUSR1, SIGUSR2, SIGIO]);
12. 支持接收重复的 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]);
13. Redis Server 支持了递归嵌套结构
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);
});
14. 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);
15. 增加 Process::getAffinity()
可获取当前进程的CPU亲缘性设置。
16. 增加更多 Process\Pool 事件回调和属性
- ? onStart:进程池启动时回调
- ? onShutdown:进程池终止时回调
- ? onWorkerExit:异步模式的工作进程即将退出时回调
- ? workerPid:当前工作进程的PID
- ? workerId:当前工作进程的ID
- ? running:进程池是否处于运行状态,在收到SIGTERM信号后将切换为false
- ? workerRunning:当前工作进程是否处于运行状态,在收到SIGTERM信号后将切换为false
转发
原文来自:
https://mp.weixin.qq.com/s/Ks1x1LNTLdl5jk0sIS6V_w