PHP在传统Web开发中主要依赖PHP-FPM多进程模型处理请求,当脚本执行到数据库查询或外部接口调用时,整个进程会被操作系统挂起,直到IO返回才能继续。这种同步阻塞方式在高并发场景下会造成大量进程堆积,CPU利用率低而内存开销大。Swoole扩展提供的协程能力,允许在单线程内通过用户态调度实现逻辑上的并发,从而在等待IO时自动切换去执行其他任务。

一、Swoole协程的底层原理
协程(Coroutine)是一种比线程更轻量的执行单元,由用户代码而非操作系统内核进行调度。Swoole在底层使用C语言实现了协程栈和调度器,当PHP代码调用Swoole提供的协程客户端(如mysql、redis、http)进行IO操作时,扩展会自动将当前协程让出(yield),把CPU时间交给调度器去运行其他就绪协程。等IO事件完成,再恢复(resume)原协程继续执行。
这种方式下,一个Swoole工作进程内可以同时存在成千上万个协程,而每个协程初始仅占用少量内存(默认约8KB栈空间)。相比FPM每开一个进程就要分配几十MB内存,协程模式极大地提升了单机连接处理能力。需要注意的是,Swoole协程是单线程内的协作式调度,不能利用多核,因此通常配合多个worker进程一起使用。
二、开启协程环境的基本方式
在使用Swoole协程前,必须确保PHP安装了Swoole扩展且版本不低于4.0。最基础的写法是通过EventLoop和协程容器来运行。SwooleCoroutinerun()会创建一个协程容器,在其中可以使用协程相关API。
<?php
// 创建协程容器并运行
SwooleCoroutinerun(function () {
// 在容器内创建一个子协程
SwooleCoroutine::create(function () {
echo "协程1开始n";
// 模拟非阻塞睡眠,让出CPU
SwooleCoroutine::sleep(1);
echo "协程1结束n";
});
SwooleCoroutine::create(function () {
echo "协程2开始n";
SwooleCoroutine::sleep(1);
echo "协程2结束n";
});
});
echo "主进程退出n";
上述代码中两个协程几乎同时开始,各自sleep一秒后结束,总耗时约一秒而非两秒。这说明在IO等待期间,调度器成功切换到了另一个协程。实际项目中我们更多使用Swoole的Server或HttpClient等高级封装,但理解run和create的容器概念是避免协程外调用报错的关键。
三、用协程客户端提升并发请求性能
最常见的性能瓶颈是串行调用多个第三方HTTP接口。在FPM中只能依次curl,总耗时是各接口之和。使用Swoole协程HTTP客户端,可以把串行改为并行。
<?php
SwooleCoroutinerun(function () {
$urls = [
'https://ipipp.com/api/user',
'https://ipipp.com/api/order',
'https://ipipp.com/api/product'
];
$results = [];
$wg = new SwooleCoroutineWaitGroup();
foreach ($urls as $i => $url) {
$wg->add();
SwooleCoroutine::create(function () use ($url, $i, &$results, $wg) {
$client = new SwooleCoroutineHttpClient('ipipp.com', 443, true);
$client->setHeaders(['Host' => 'ipipp.com']);
$client->get('/api/' . basename($url));
$results[$i] = $client->body;
$client->close();
$wg->done();
});
}
$wg->wait(); // 等待所有协程完成
var_dump($results);
});
这里使用了WaitGroup来等待一批协程全部结束,三个接口请求在协程中并发进行,总耗时取决于最慢的那个而非三者相加。在内部服务聚合网关、批量数据拉取等场景中,这种写法能将接口响应时间从几百毫秒降到几十毫秒。
不过要注意,协程客户端只能替换Swoole自带的IO方法。如果在协程内使用了file_get_contents访问网络、或者调用了未兼容协程的阻塞扩展(如某些旧版PDO),会导致整个worker进程卡死。此时应改用SwooleCoroutineSystem::getaddrinfo或协程化的MySQL客户端。
四、常见误区与避坑建议
第一个误区是在协程中滥用全局变量。由于协程是交替执行的,如果多个协程修改同一个全局数组且未加锁,会产生数据竞争。建议使用上下文隔离的SwooleCoroutine::getContext()来保存协程私有数据。
<?php
SwooleCoroutinerun(function () {
SwooleCoroutine::create(function () {
$ctx = SwooleCoroutine::getContext();
$ctx->uid = 1001;
SwooleCoroutine::sleep(0.1);
echo "协程内uid=" . $ctx->uid . "n";
});
});
第二个误区是认为开了协程就能自动加速CPU密集型任务。协程只解决IO等待问题,如果是复杂计算,仍会阻塞当前进程。此类任务应放到Swoole的Task进程或用多进程处理。掌握协程适用边界,才能用PHP+Swoole真正构建高并发服务。
| 模式 | 并发单位 | 阻塞表现 | 典型QPS |
|---|---|---|---|
| PHP-FPM | 进程 | 进程挂起 | 数百 |
| Swoole协程 | 协程 | 让出CPU | 数千至上万 |
五、小结
通过Swoole协程,PHP开发者可以用近乎同步的写法获得异步非阻塞的性能收益。核心步骤包括:安装Swoole扩展、在协程容器内编写逻辑、用协程客户端替换阻塞IO、用WaitGroup管理批量任务。只要避开全局变量混用和阻塞函数误用,就能以很小成本将老系统并发能力提高数倍。