PHP的yield不是协程并发,因其不支持自动挂起I/O和调度;真协程需Swoole等扩展提供非阻塞I/O、独立上下文及错误可见性。

PHP 本身没有原生协程调度器,所谓“协程并发”实际依赖扩展(如 Swoole、OpenSwoole)或用户态库(如 Amp、ReactPHP),直接用 yield 写的生成器不是可并发调度的协程——它不会自动挂起 I/O,也不会让出控制权给其他任务。
为什么 yield 不等于协程并发
PHP 的 yield 只是生成器语法糖,它不感知网络 I/O、文件读写或 sleep 等阻塞操作。下面这段代码仍是串行执行:
function task1() {
yield sleep(2); // 这里会真正阻塞整个进程 2 秒
echo "task1 done\n";
}
function task2() {
yield usleep(100000);
echo "task2 done\n";
}
// 两个 generator 依次运行,无并发
foreach ([task1(), task2()] as $g) {
foreach ($g as $_) {}
}
常见误解是“用了 yield 就是协程”,其实只是有了协程的壳,缺了调度器和非阻塞 I/O 支持。
-
yield本身不触发上下文切换,也不注册事件监听 - 所有标准 PHP 函数(
fread、curl_exec、sleep)默认都是阻塞的 - 要实现并发,必须用扩展提供的非阻塞替代品,比如
Swoole\Coroutine\sleep或Amp\delay
用 Swoole 实现真协程并发的关键三步
Swoole 是目前最成熟的 PHP 协程方案,它的 Swoole\Coroutine 模块提供了完整的用户态调度器。但必须按规范使用,否则照样退化为同步。
立即学习“PHP免费学习笔记(深入)”;
- 入口必须用
Swoole\Coroutine\run或在Swoole\Http\Server等协程环境中启动 - 所有 I/O 操作必须替换为协程版本:用
Swoole\Coroutine\MySQL代替mysqli,用Swoole\Coroutine\Http\Client代替cURL - 禁止混用阻塞函数:
file_get_contents、sleep、stream_socket_client在协程中会阻塞整个协程调度器
示例:并发请求两个 URL
Swoole\Coroutine\run(function () {
$chan = new Swoole\Coroutine\Channel(2);
go(function () use ($chan) {
$client = new Swoole\Coroutine\Http\Client('httpbin.org', 80);
$client->get('/delay/1');
$chan->push($client->body);
});
go(function () use ($chan) {
$client = new Swoole\Coroutine\Http\Client('httpbin.org', 80);
$client->get('/delay/1');
$chan->push($client->body);
});
echo $chan->pop() . "\n"; // 并发完成,总耗时约 1 秒而非 2 秒
echo $chan->pop() . "\n";
});
协程上下文丢失的典型场景
协程是轻量级线程,但 PHP 的超全局变量($_GET、$_SERVER)、静态属性、global 变量在协程间是共享的,容易引发状态污染。
-
$_SERVER['request_time_float']在多个协程中可能被覆盖——Swoole 提供Swoole\Coroutine::getContext()和Co::pack/Co::unpack做隔离,但更推荐显式传参 - 使用
static $cache = []缓存数据时,不同协程会共用该数组,导致脏读;应改用Co::getUid()做键前缀,或直接用Swoole\Coroutine\Channel隔离 - ORM 如 Laravel Octane + Swoole 场景下,
DB::connection()返回的连接对象若未设为协程安全,可能复用到其他请求的连接
协程错误无法穿透到父作用域
协程内抛出的异常默认不会冒泡到 go() 调用处,而是被调度器捕获并静默丢弃(除非开启 swoole.display_errors=On 或设置 Swoole\Coroutine::set(['hook_flags' => SWOOLE_HOOK_EXCEPTION]))。
- 调试时看不到报错?先检查
ini_set('display_errors', '1')和swoole.display_errors是否开启 - 生产环境建议统一用
try/catch包裹每个go内部逻辑,并通过Channel或defer传递错误 - 不要依赖
set_exception_handler捕获协程异常——它只对主线程有效
协程模型的复杂性不在语法,而在执行边界是否清晰:I/O 是否非阻塞、上下文是否隔离、错误是否可见。这三个点没理清,再多的 go 和 yield 也只是一层幻觉。



















