Hyperf 的 Command 默认不运行在协程环境中,因其入口以同步模式启动,handle() 方法执行于非协程上下文;必须显式启用协程调度(如 Co::create)并使用协程安全组件(如 CoroutineHttpClient、StdoutLoggerInterface),否则会阻塞或报错。

Hyperf 的 Command 默认不运行在协程环境中,直接写协程代码会阻塞主线程、无法并发、甚至报 Co::sleep(): must be called in the coroutine 错误——这不是你代码写得不对,是启动方式没对。
Hyperf Command 为什么默认不进协程
Hyperf 的命令行入口(bin/hyperf.php)调用 Application::run() 时,默认以同步模式启动,Command 类的 handle() 方法是在主进程、非协程上下文中执行的。哪怕你在里面用了 go 或 Co::sleep,底层也没开启协程调度器。
- Hyperf 2.x/3.x 均如此,不是 bug,是设计使然:CLI 场景多数不需要协程,避免隐式开销
-
Stdout、Input、Output组件本身非协程安全,混用易出错 - 即使手动
Swoole\Coroutine::create()启动,也绕不开 I/O 阻塞和上下文丢失问题
让 Command 进协程的两种可靠方式
必须显式启用协程环境,并确保所有 I/O 操作走协程版驱动。推荐以下任一路径:
- 使用
Hyperf\Contract\StdoutLoggerInterface替代Output,避免同步输出卡住协程 - 改用
Hyperf\Process\ProcessManager启动独立协程进程(适合长时任务),但需注意信号传递和退出逻辑 - 最简方案:在
handle()开头加Co::set(['hook_flags' => SWOOLE_HOOK_ALL]);+Co::create(fn() => $this->doWork());,但仅限纯协程 I/O(如Co\Http\Client、Co\MySQL),不能混用PDO或file_get_contents
示例片段:
public function handle()
{
Co::set(['hook_flags' => SWOOLE_HOOK_ALL]);
Co::create(function () {
// ✅ 协程 MySQL 查询
$result = $this->mysql->query('SELECT * FROM user LIMIT 1');
// ✅ 协程 HTTP 请求
$client = new Co\Http\Client('http://api.example.com', 80);
$client->get('/status');
$this->logger->info('done in coroutine');
});
}
常见踩坑点:日志、数据库、HTTP 客户端全都不一样了
协程环境下,所有资源必须“按协程路子走”,否则轻则性能归零,重则崩溃或数据错乱:
-
LoggerInterface要用StdoutLoggerInterface或自定义协程安全的 logger,普通Monologhandler 会锁住整个协程调度 -
Db组件必须配置pool.min_connections = 1且启用cooperative模式(Hyperf 3.1+ 默认支持),否则mysql连接池会复用失败 -
Http\Client必须用Hyperf\HttpClient\CoroutineHttpClient,而非 Guzzle 或 cURL 封装 —— 后者仍是同步阻塞 - 别在
handle()里sleep(1),要Co::sleep(1);别用file_put_contents,改用Co::writeFile
复杂任务建议拆到 Process 或 Job 中执行
真正需要高并发、长时间运行的 CLI 任务(比如批量导出、定时清洗),不要硬塞进 Command —— 它本质是调试/运维入口,不是任务执行引擎。
- 把核心逻辑封装成
Job,用Hyperf\AsyncQueue\Driver\RedisDriver投递,由async-queue:consume进程处理 - 或注册为
Process,在onStart中启动协程调度器,用Co::waitGroup管理子任务生命周期 -
Command只做参数校验、触发投递、打印状态,保持轻量
容易被忽略的是:Command 进程退出后,它 spawn 的协程若未显式 Co::join() 或等待完成,会被强制终止,导致任务静默丢弃。


















