子协程拿不到父协程的Context数据,是因为Swoole协程上下文默认不继承,每个协程ID对应独立存储空间,Hyperf\Context\Context基于当前协程ID读写,故子协程需通过Context::copy()、根协程ID存储或RpcContext透传等方式显式共享数据。

在Hyperf 3.1中实现跨协程的上下文数据传递时,常因误以为Context会自动继承而出现子协程读不到父协程设置的user_id、trace_id等关键字段的问题,这直接导致日志错乱、权限校验失效或RPC调用丢失元信息。
为什么子协程拿不到父协程的Context数据
协程上下文默认不继承——Swoole为每个协程分配独立的内存空间,Hyperf\Context\Context底层调用Coroutine::getContext()时传入的是当前协程ID,子协程ID不同,查不到父协程写入的数据。
这一步不是bug,是设计使然:若自动继承,高并发下上下文数据污染风险极高,比如A请求设了$user_id=1001,B请求刚创建的子协程可能误读该值。
【关键前提】Hyperf 3.1中Context::set()写入的数据仅对当前协程ID可见,跨协程必须显式传递或改用根协程ID存储。
安全传递上下文的三种方法
方法一:手动复制上下文(推荐用于简单场景)
在go()前调用Context::copy()获取当前上下文快照,再作为参数传入协程函数:
go(function($ctx) { Context::set('user', $ctx['user'] ?? null); handleAsyncTask(); }, Context::all());
注意:Context::all()返回的是当前协程全部键值对,不含嵌套对象引用,避免序列化失败。
方法二:使用根协程ID存储(适用于需多层子协程共享)
先定义根协程ID获取逻辑:
function getRootCid(): int { $cid = Coroutine::getCid(); while ($pcid = Coroutine::getPcid($cid)) { $cid = $pcid; } return $cid; }
然后统一用getRootCid()作为Context::set()的上下文标识:
Context::set(getRootCid(), 'user_id', 123); // 所有子协程都可通过getRootCid()读取
方法三:通过RPC上下文透传(专用于JSON-RPC调用链)
在客户端调用前,将数据注入RpcContext:
RpcContext::set('x-request-id', $request->getHeaderLine('x-request-id'));
服务端在@RpcService注解方法内直接取:
$requestId = RpcContext::get('x-request-id');
该方式由Hyperf RPC中间件自动完成跨进程上下文携带,无需手动序列化。
必须避开的三个致命陷阱
第一步:禁止在单例Service中用protected成员存请求数据
例如$this->currentUserId = $id;会导致后续任意协程读到上一个请求残留的ID——因为Service实例在Worker进程内常驻,而protected属性不随协程切换重置。
第二步:不要在TaskWorker里尝试还原Context
task进程与worker是隔离操作系统进程,$this、RequestInterface、Db连接等全部不可序列化;投递时只允许传标量、数组、DTO对象,【传闭包或$this必然报Serialization of 'Closure' is not allowed】。
第三步:警惕static变量伪装成“协程安全”
static $cache = [];在Swoole中等于进程级全局变量,100个并发协程共用同一份数组,$cache[] = $data会相互覆盖,压测时错误率陡增。


















