hyperf默认不自动继承父协程上下文,子协程通过coroutine::create()启动时拥有全新空上下文,需显式调用context::copy()才能获取父协程context::get数据。

子协程为什么拿不到父协程的 Context::get 数据
Hyperf 默认不自动继承父协程上下文 —— Context::set() 写入的数据只存在于当前协程 ID 的上下文中,Coroutine::create() 或 go() 启动的新协程拥有全新、空的上下文,和父协程完全隔离。这不是 bug,是设计使然:避免隐式数据传递带来的不可控副作用。
常见错误现象:
- 父协程调用
Context::set('user_id', 123),子协程里Context::get('user_id')返回null - 中间件里存了 token,控制器里能取到;但控制器内再
go(function () { ... }),里面就取不到了 - 日志 trace_id 在主协程设了,异步任务里打出来的日志没 trace_id,链路断掉
Context::copy 是什么,什么时候必须用它
Context::copy() 不是“复制变量”,而是把当前协程的整个上下文数组(键值对)浅拷贝到目标协程 ID 中。它解决的是「显式传递请求级上下文」这个明确需求,不是用来兜底或替代设计的。
使用场景很具体:
- 你手动调用
Coroutine::create($cid, $func)并指定了协程 ID - 你在非 Hyperf 标准调度路径下启动协程(比如自定义任务队列消费者、定时器回调里启协程)
- 你需要子协程读写与父协程**同一份上下文快照**(注意:后续父协程改,子协程不会同步更新)
示例:
// 父协程
Context::set('trace_id', 'req_abc123');
Context::set('user_id', 456);
$childCid = Coroutine::create(function () {
// 子协程默认拿不到上面两个值
var_dump(Context::get('trace_id')); // null
});
// 正确做法:copy 父协程上下文到子协程
Context::copy(Coroutine::id(), $childCid);
比 Context::copy 更自然的替代方案
90% 的业务场景其实不该手动 copy。Hyperf 已经在关键路径做了自动继承:
-
go()启动的协程:自动继承父协程上下文(Hyperf 2.2+ 默认行为) - 通过
Pool、AsyncQueue、Job等 Hyperf 官方组件启动的任务:内部已封装Context::copy()或等效逻辑 - 中间件 → 控制器 → 服务层 → 子协程调用链:只要没跳出 Hyperf 调度,上下文天然延续
所以,如果你发现子协程拿不到上下文,先确认是不是用了裸 Coroutine::create();如果是,才需要 Context::copy();如果用了 go() 还拿不到,大概率是协程已结束或上下文被提前清空(比如响应返回后框架主动清理)。
容易踩的坑:copy 之后改了父协程,子协程会同步变吗
不会。Context::copy() 是一次性浅拷贝,之后父子协程上下文完全独立。这既是安全机制,也是易错点:
- 父协程改
Context::set('status', 'done'),子协程里的status仍是 copy 当时的旧值 - 子协程自己
Context::set('log_time', time()),不影响父协程 - 如果需要双向通信或实时同步,应该用
Swoole\Coroutine\Channel或Swoole\Atomic,而不是依赖上下文
真正容易被忽略的是:上下文不是“全局状态容器”,它只是协程生命周期内的临时数据槽。一旦协程结束,对应上下文自动销毁 —— 所以别在协程外试图保留或复用协程 ID 来取数据。











