首先需要明确一个核心观点:使用Swoole协程处理CPU密集型任务,不仅无法获得性能提升,反而可能导致整个服务瘫痪。这与许多人理解的“并发能力”概念存在本质区别。

协程与CPU密集型计算之间,天然存在不可调和的矛盾。
为什么协程在CPU密集场景下会失效
协程的核心机制基于协作式调度,即协程主动让出CPU资源。然而,纯计算型操作——例如大规模循环、矩阵运算、正则表达式回溯——完全不涉及任何IO事件,像co::sleep()、co::read()这类挂起点根本不会被执行,调度器无法获得介入机会。最终结果是一个协程彻底独占整个Worker进程的CPU时间片,其他协程只能等待。
几个直观的典型表现:
- 即便设置
worker_num=8,在 top 中可以看到 8 个 PHP 进程,但其中 1 个 CPU 占用率达到 100%,其余 7 个几乎空转 - 根本原因在于 PHP 是单线程解释器,所有协程都在同一个 OS 线程中运行,无法利用多核并行计算能力
- 常见误区是认为使用
go(function () { heavy_calc(); })就能实现“并发执行”,实际上协程只是排队等待同一个 CPU 资源,没有任何并发收益
task_worker_num 是唯一可靠的解决方案
真正能够隔离CPU密集型任务的方案,唯有 task_worker_num。它启动的是独立子进程(而非线程),每个 task worker 拥有完整的 PHP 解释器和独立的内存空间,天然绕开了单线程瓶颈。
几个必须注意的关键细节:
- 必须在
swoole_server->set()中显式设置task_worker_num >= 1,设为 0 或不设置则相当于未启用 - 投递任务必须使用
$server->task($data),如果在onReceive中直接调用函数,则等于白费配置 - 在
onTask回调中可以放心使用usleep()、json_decode()、shell_exec(),这些操作不会阻塞主 Worker - 但需注意,task 进程之间不共享
global、static变量,每次调用都会创建全新的实例
declare(tick=N) 抢占式调度仅理论可行,生产环境慎用
PHP 的 declare(ticks=N) 在理论上可以实现协程级抢占,但代价高昂且不实用。每执行 N 条低级语句就会触发一次 handler,计算速度被严重拖慢,并且 tick 无法覆盖所有语句(如类定义、方法声明),边界情况极难控制。
- 压测数据足以说明问题:1000 万次
$i++在ticks=100设置下,耗时增加约 40%,CPU 效率大幅下降 - 在 handler 中增加时间判断并调用
co::yield(),会引入额外上下文切换开销,反而放大延迟 - Swoole 官方从未推荐过此方案,企业级服务中它基本只出现在概念验证演示中
OpenCPU 或远程计算才是高负载场景的真正出路
当计算任务复杂度超出单机能力——例如模型推理、批量图像处理——单纯依靠 task worker 已不现实。此时应将计算逻辑外移,通过 HTTP 或 Socket 异步委托给专用计算节点。
- Swoole 端只需使用
SwooleCoroutineHttpClient发起请求,全程协程安全 - OpenCPU 方案提供沙箱隔离、资源限制、自动扩缩容,比自建 task worker 更稳定且省心
- 关键点在于必须设定超时(
timeout)、重试(retry)和降级逻辑,防止计算服务不可用时拖垮网关
最后提及一个容易被忽略的点:许多看似“CPU密集”的问题,实际上属于伪命题。例如未加索引的 MySQL 全表扫描、未设置超时的 cURL、同步写入机械硬盘的日志——这些才是真正的性能杀手。先确认瓶颈所在,再决定采用 task 还是调整架构,切勿一上来就堆叠子进程。
