hyperf 的 waitgroup 是协程安全的计数器封装,仅能在协程中使用,非协程环境调用 wait() 会报错;必须成对调用 add()/done(),不可复用,且不传播异常。

Hyperf 的 WaitGroup 不是 Go 风格的阻塞式同步原语,它本质是协程安全的计数器 + Co::wait 封装,不能在非协程环境或同步上下文中“真正等待”——直接当 Go 的 sync.WaitGroup 用会卡死或报错。
WaitGroup 必须运行在协程中,且不能用于同步代码
Hyperf 的 HyperfUtilsWaitGroup 底层依赖 SwooleCoroutine::wait(),而该函数仅在协程内有效。若在 __construct、命令行同步逻辑、或 onWorkerStart 等非协程上下文中调用 $wg->wait(),会触发 Fatal error: Uncaught SwooleException: swoole_coroutine_wait(): must be called in the coroutine。
- ✅ 正确场景:HTTP 控制器、
@AsyncTask、go()启动的协程内 - ❌ 错误场景:普通 PHP CLI 脚本、
config/autoload/中的配置文件、Command类的handle()(未显式启动协程时) - ⚠️ 注意:
wait()是协程挂起操作,不是忙等;它会让出当前协程控制权,直到计数归零
add() 和 done() 的调用必须成对,且不能在 wait() 后再调用 done()
WaitGroup 的计数器是严格递增/递减的,done() 调用次数超过 add(n) 初始值会导致 InvalidArgumentException: counter can not be less than zero。常见于异常分支遗漏 done(),或重复调用 done()。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
- ✅ 推荐写法:用
try/finally包裹异步操作,确保done()总被执行 - ❌ 危险写法:
if ($err) return;后直接结束,没调done() - ⚠️ 不支持“动态重置”:一旦
wait()返回,该WaitGroup实例不可复用,需新建
go(function () {
$wg = new HyperfUtilsWaitGroup();
$wg->add(2);
go(function () use ($wg) {
try {
// 模拟异步请求
$data = json_decode((string)(yield HyperfHttpClientHttpClientFactory::create()->get('https://httpbin.org/delay/1')), true);
var_dump($data['url'] ?? 'ok');
} finally {
$wg->done();
}
});
go(function () use ($wg) {
try {
co::sleep(0.8);
var_dump('done after sleep');
} finally {
$wg->done();
}
});
$wg->wait(); // 协程在此挂起,直到两个 done() 被调用
var_dump('all done');
});
WaitGroup 不处理错误传播,需自行捕获和透出异常
Go 的 WaitGroup 本身不关心子 goroutine 是否 panic,Hyperf 的实现同理:wait() 只等计数归零,不管协程里是否抛出异常。未捕获的 Throwable 会直接打印到日志并终止该协程,但主流程仍可能继续执行(甚至 wait() 提前返回)。
- ✅ 建议:每个
go()内部用try/catch捕获异常,并通过共享变量(如AtomicInteger或引用数组)收集错误 - ❌ 依赖
wait()自动聚合异常:它不会做这件事 - ⚠️ 注意:Swoole 协程中
throw不会跨协程传播,别幻想用set_exception_handler统一捕获
Hyperf 的 WaitGroup 是轻量协作工具,不是并发控制银弹。它的价值在于明确表达“我需要等 N 个协程完成”,而不是替代 Channel、Promise 或结构化异常处理。用错上下文、忽略计数守恒、或期待自动错误收敛,是三个最常导致行为诡异的点。










