协程并行不等于“加go就快”,根本原因在于底层调用若非协程安全(如原生curl、PDO),会阻塞整个worker,导致表面并行实则串行排队;必须使用协程化组件(如hyperf/guzzle)并配合WaitGroup或Channel正确同步,同时避免隐藏的同步黑洞。

协程并行不等于“加 go 就快”
直接把原来串行的 sleep(2)、$client->get()、$db->query() 包进 go() 或 Coroutine::create(),不一定提速,甚至更慢。根本原因是:如果底层调用不是协程安全的(比如用 curl_exec、PDO::query),那这个协程一执行就会阻塞整个 worker,其他协程全卡住——表面是“并行”,实际是“排队等一个同步调用结束”。
必须确认你用的组件是否协程化:
-
hyperf/guzzle(不是原生 Guzzle)✅ -
hyperf/db(基于 Swoole MySQL 协程客户端)✅ -
hyperf/redis(封装了Swoole\Coroutine\Redis)✅ - 原生
curl、file_get_contents、PDO❌
用 WaitGroup 控制并行边界和收口
多个协程并发发请求后,不能靠 usleep() 或随机 sleep() 等结果。正确做法是用 Hyperf\Utils\WaitGroup 显式等待全部完成,避免竞态或提前返回空数据。
常见错误写法:
$wg = new \Hyperf\Utils\WaitGroup();
$wg->add(2);
go(function () use ($wg) {
$this->clientFactory->create()->get('https://api.a');
$wg->done();
});
go(function () use ($wg) {
$this->clientFactory->create()->get('https://api.b');
$wg->done();
});
// ❌ 这里没等,直接 return,结果为空
return 'done';
正确写法:
$wg = new \Hyperf\Utils\WaitGroup();
$wg->add(2);
$responses = [];
go(function () use ($wg, &$responses) {
$res = $this->clientFactory->create()->get('https://api.a');
$responses['a'] = (string) $res->getBody();
$wg->done();
});
go(function () use ($wg, &$responses) {
$res = $this->clientFactory->create()->get('https://api.b');
$responses['b'] = (string) $res->getBody();
$wg->done();
});
$wg->wait(); // ✅ 阻塞当前协程,直到两个子协程都调用了 done()
return $responses;
Channel 适合需要按序消费或流式传递结果的场景
如果你的并行任务产出结果有先后依赖、需要合并、或要控制消费节奏(比如只取前 N 条),Swoole\Coroutine\Channel 比全局数组 + WaitGroup 更合适,也更符合协程调度语义。
注意点:
-
$channel->push()是协程安全的,但若缓冲区满(默认容量为 1),会阻塞当前协程,直到有协程pop()—— 所以建议显式设容量:new Channel(10) - 不要在非协程环境(如命令行脚本未启动 event loop)中用
Channel,会报Invalid argument: channel is not available - 多个生产者往同一
Channel推数据时,消费者pop()获取顺序取决于协程调度,不保证与push顺序一致
concurrent.limit 不是协程数,别和并行任务数混淆
你在 Controller 里用 go() 启了 50 个协程,并不意味着异步队列的 concurrent.limit 就该设成 50。这两个是不同层级的并发控制:
-
go()属于“内存内协程调度”,开多少看内存和 I/O 密集度,上限常是几百 -
concurrent.limit是“队列消费者进程内 handle() 方法的并发槽位数”,它限制的是真正同时跑在 DB/Redis/HTTP 客户端上的任务数
举个典型误配:
你写了 10 个 go() 并行查用户信息,每个查库耗时 80ms,但异步队列的 concurrent.limit = 1 —— 结果所有任务排队串行执行,总耗时 ≈ 800ms,比串行还慢(多了协程调度开销)。
所以,当你的业务逻辑本身已用协程并行,又走异步队列时,concurrent.limit 应该按单个任务真实耗时反推,而不是按你写了几个 go()。
真正容易被忽略的是:协程并行提速的前提,是整个调用链路没有“隐藏的同步黑洞”——比如某个中间件里调了 file_get_contents,或者日志驱动用了同步写文件,都会让整条链路退化。得逐层检查,不能只盯着 go() 和 Channel。


















