在已有协程中反复调用 go() 会导致协程数量爆炸式增长,迅速耗尽内存并可能触发 oom 或进程被 kill;go() 应仅在入口层(如请求处理、定时任务)一次性调用,避免在循环或嵌套逻辑中使用。

协程里再调用 go() 会怎样?
直接在已有协程中反复调用 go(),尤其是放在循环体里(比如 while 或 for),会导致协程数量不受控地爆炸式增长。Swoole 不限制协程创建数量,但每个协程至少占用几 KB 栈空间,大量协程会迅速耗尽内存,还可能触发内核 RLIMIT_STACK 限制,最终进程被 kill 或 OOM。
常见错误写法:
go(function() {
while (true) {
go(function() { // ⚠️ 这里每轮都新建一个协程!
Co::sleep(1);
});
Co::sleep(0.1);
}
});
- 这不是“并发控制”,而是资源泄漏
- 协程 ID(
Co::getuid())会持续递增,可用来监控是否失控 - Worker 进程 RSS 内存持续上涨,但 CPU 可能不高——因为大部分协程在 sleep 等待
go() 应该只在入口层调用
go() 的合理位置是请求入口、定时任务回调、连接建立后等“一次性的启动点”,而不是业务逻辑内部。真正需要并发执行的 I/O 操作,应封装成函数后统一交给 go() 启动,而非在循环中逐个启动。
正确做法示例(HTTP 请求批量处理):
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
// ✅ 入口层一次性启动多个协程
go(function() {
$urls = ['https://a.com', 'https://b.com', 'https://c.com'];
$clients = [];
foreach ($urls as $url) {
go(function() use ($url) {
$client = new Co\Http\Client(parse_url($url, PHP_URL_HOST), 443, true);
$client->get(parse_url($url, PHP_URL_PATH) ?: '/');
echo "done: {$url}\n";
});
}
});
- 避免在
foreach里嵌套go()的变体:比如array_map('go', ...)同样危险 - 如果需动态控制并发数(如限流 5 个并发),用
Channel或WaitGroup协调,而不是靠循环启协程 -
Co::set(['hook_flags' => SWOOLE_HOOK_ALL])必须提前开启,否则file_get_contents等同步调用会阻塞整个 Worker
用 Channel 或 WaitGroup 替代循环启协程
当你要“对一批数据并发处理并等待结果”时,Channel 和 WaitGroup 是更安全、可控的替代方案。它们不新增协程,只协调已有协程的生命周期。
例如限制 3 个并发 HTTP 请求:
$chan = new Co\Channel(3); // 缓冲大小=并发数
$urls = range(1, 10);
foreach ($urls as $i) {
$chan->push($i); // 把任务投进通道
}
go(function() use ($chan) {
while ($id = $chan->pop()) {
// 实际处理逻辑,每个协程串行消费任务
Co::sleep(rand(0.1, 0.5));
echo "handled {$id}\n";
}
});
// 启动 3 个消费者协程
for ($i = 0; $i handle($chan)); // ✅ 这里才调用 go,且固定数量
});
-
Channel天然带背压,不会无限创建协程 -
WaitGroup更适合“启动 N 个协程 → 等全部结束”,但要注意add()必须在go()外调用,否则竞态 - 别在
go()回调里再调go(),这是最容易忽略的嵌套陷阱
Worker 进程重启后协程状态不会延续
很多人误以为协程像全局变量一样能在多次请求间复用,其实不然:go() 创建的协程只属于当前请求或当前 Co::run() 生命周期。Worker 进程 reload 或异常退出后,所有协程立即销毁,不会残留。
- 所以“循环创建协程导致内存不释放”的问题,只发生在单次请求/单个
Co::run()内 - 但若在
onWorkerStart中误写死循环 +go(),就会让 Worker 一启动就失控 - 检查方法:
ps aux | grep swoole看 RSS;或用cat /proc/PID/status | grep VmRSS
最隐蔽的坑是把 go() 放在框架中间件或公共函数里,被多层调用链无意触发——务必确认调用栈深度和触发频率。










