WaitGroup是协程同步原语,仅在协程上下文中有效;非协程环境调用wait()会阻塞进程,go()启动的协程无法调度,且不解决共享变量竞争问题。

WaitGroup 本质是协程同步原语,不是进程/线程同步工具
WaitGroup 在 Hyperf 里不是用来等「多个 PHP 进程」或「多个系统线程」的,它只对当前协程上下文内的子协程起作用。它的底层靠 Co\Channel 实现,所有 add()、done()、wait() 调用都必须发生在同一个协程生命周期内——一旦主协程退出(比如 HTTP handler 返回),它创建的所有子协程若未完成,会被强制回收,而 WaitGroup 根本不会感知到,更不会阻塞主协程继续执行。
在非协程环境调用 WaitGroup.wait() 会直接阻塞整个进程
如果你在传统 PHP-FPM 或命令行同步脚本里写:
$wg = new \Hyperf\Utils\WaitGroup();
$wg->add(1);
go(function () use ($wg) {
usleep(100000);
$wg->done();
});
$wg->wait(); // ❌ 这里会卡死,因为 wait() 是协程调度等待,但当前不在协程中
结果不是超时,而是整个进程挂起,CPU 占用归零,无任何报错。原因很简单:wait() 底层调用的是 Co\Channel::pop(),而该操作仅在协程中可 yield;在非协程上下文中,它退化为一个无法返回的空循环。
- Hyperf 的
WaitGroup类型不兼容同步上下文,没有 fallback 行为 -
go()启动的协程在非协程环境里根本不会被调度,done()永远不执行 - 不要试图在
Command类的handle()方法里直接用WaitGroup,除非你明确用co()包裹整个逻辑
HTTP handler 中漏掉协程包裹是高频错误点
很多人写接口时这样写:
public function index()
{
$wg = new \Hyperf\Utils\WaitGroup();
$wg->add(2);
go(function () use ($wg) { /* ... */ $wg->done(); });
go(function () use ($wg) { /* ... */ $wg->done(); });
$wg->wait(); // ✅ 看似没问题,但前提是这个方法本身运行在协程中
return 'done';
}
这能跑通,是因为 Hyperf 的控制器方法默认由协程驱动。但隐患在于:一旦你把这个逻辑抽成普通 service 方法,并在非协程上下文(如单元测试、定时任务 Command)里调用,wait() 就立刻失效。真正安全的写法是:
- 所有含
go()+WaitGroup的逻辑,必须显式用co()或go()启动顶层协程 - 避免把
WaitGroup实例传给不确定执行环境的函数 - 如果要复用逻辑,封装成返回
Promise或Coroutine\Result的协程函数,而不是裸露WaitGroup
WaitGroup 不解决跨协程上下文的数据竞争
它只保证「计数器归零」,不保证变量安全。例如:
$data = [];
$wg = new \Hyperf\Utils\WaitGroup();
$wg->add(2);
go(function () use ($wg, &$data) {
$data[] = 'a'; // ❌ 共享变量,PHP 数组写入非原子操作
$wg->done();
});
go(function () use ($wg, &$data) {
$data[] = 'b';
$wg->done();
});
$wg->wait();
// $data 可能是 ['a','b'],也可能是 ['b','a'],甚至(极小概率)损坏
这类问题不会报错,但结果不可预测。需要改用 Co\Channel 传递数据,或用 Atomic / Table 管理共享状态。
WaitGroup 最容易被当成“万能并发等待工具”,但它只管一件事:等子协程结束。是否安全、是否有序、是否资源隔离,全得靠你手动设计。协程不是魔法,只是把调度权从内核交到了用户代码手里——责任也一并移交了。


















