hyperf协程间数据共享需依场景选方案:实时传递用channel(注意容量),最终聚合用waitgroup(须调done),上下文透传用context::copy(),简单并行用parallel(自动处理上下文与异常)。

Hyperf 协程之间不能靠全局变量或静态属性共享数据,因为每个协程有独立 CID 和隔离的 Context,父协程写入的 Context::set() 在子协程里默认读不到——这不是 bug,是设计使然。
用 Channel 实现生产者-消费者式数据传递
这是最直接、最可控的方式,适用于异步任务结果收集、并发 IO 后聚合等场景。注意 Channel 容量必须 ≥ 预期写入次数,否则 push() 会阻塞或丢数据。
-
Channel是协程安全的 FIFO 队列,push()和pop()都会自动挂起协程等待对方就绪 - 主线程必须在所有子协程
push完成后再pop,否则可能提前读空或死锁 - 不要混用
Swoole\Coroutine\Channel和Hyperf\Utils\Channel,前者更底层、兼容性更好
$channel = new \Swoole\Coroutine\Channel(10);
co(function () use ($channel) {
$data = Db::select('SELECT id FROM users LIMIT 5');
foreach ($data as $row) {
$channel->push($row->id);
}
});
// 主协程等待并消费
$result = [];
for ($i = 0; $i pop();
}
用 WaitGroup 等待协程完成并合并结果
适合不需要实时传递、只需最终汇总结果的场景,比如并发调用多个 HTTP 接口后拼装响应。它不传数据,但能确保主协程拿到所有子协程的执行结果。
-
WaitGroup本身不携带数据,需配合&$ref或闭包外变量收集结果 - 必须在每个子协程末尾调用
$wg->done(),漏调会导致wait()永久挂起 - 避免在子协程中直接
return或抛异常跳出,否则done()不会被执行
$wg = new \Hyperf\Utils\WaitGroup();
$data = [];
$wg->add(3);
co(function () use ($wg, &$data) {
$data[] = httpGet('https://api.a');
$wg->done();
});
co(function () use ($wg, &$data) {
$data[] = httpGet('https://api.b');
$wg->done();
});
$wg->wait(); // 阻塞直到全部 done
return $data;
Context::copy() 显式继承父协程上下文
当子协程需要访问父协程设置的 trace_id、user.id 等请求级上下文时,必须显式复制,否则 Context::get() 返回 null。
-
Context::copy()只复制键值对,不复制生命周期管理逻辑(如destroy()不会自动传播) - 复制后子协程可自由
set(),不影响父协程;但父协程后续改写不会同步到子协程 - 如果嵌套层级深,建议封装一个
withContext()工具函数统一处理
$parentId = Coroutine::id();
Context::set('trace_id', 'req_abc123');
Context::set('user.id', 456);
<p>co(function () use ($parentId) {
Context::copy($parentId, Coroutine::id());
var_dump(Context::get('trace_id')); // 'req_abc123'
var_dump(Context::get('user.id')); // 456
});
</p>
Parallel 封装自动处理上下文与错误捕获
对于简单并行任务(如批量查 DB、发通知),Hyperf\Coroutine\Parallel 是最省心的选择:它内部自动做 Context::copy()、统一收集返回值、自动捕获子协程异常。
- 返回值是数组,顺序与输入任务顺序一致,无需手动维护索引
- 任意子协程抛异常,
Parallel会把异常对象放进对应位置,不会中断其他任务 - 不支持自定义 Channel 或 WaitGroup 行为,灵活性低但出错率极低
use Hyperf\Coroutine\Parallel;
<p>$parallel = new Parallel(3);
foreach (['a', 'b', 'c'] as $key) {
$parallel->add(function () use ($key) {
return Db::fetch("SELECT * FROM config WHERE type = '$key'");
});
}
$results = $parallel->wait(); // 自动 copy 上下文,自动收异常
</p>
真正容易被忽略的是:Context 隔离不是为了“不让传”,而是防止误共享;Channel 和 Parallel 虽好,但若没设对容量或没处理好异常路径,照样会卡死或丢数据。动手前先想清楚——你要的是实时流式传递,还是最终聚合,或是上下文透传?选错方式,调试成本远高于写法本身。











