协程死锁问题
实例
const SWITCH_COUNT = 100000;
function accurateCoroutineSwitchTest()
{
$start = hrtime(true);
Swoole\Coroutine\run(function () {
$channel = new Swoole\Coroutine\Channel(1);
$done = new Swoole\Coroutine\Channel(1);
// 生产者协程 (在主协程中运行)
$value = 0;
for ($i = 0; $i < SWITCH_COUNT; $i++) {
$channel->push($value);
$value = $channel->pop();
}
// 消费者协程
go(function () use ($channel, $done) {
for ($i = 0; $i < SWITCH_COUNT; $i++) {
$data = $channel->pop();
$channel->push($data + 1);
}
$done->push(true);
});
$done->pop(); // 等待消费者完成
});
$end = hrtime(true);
$totalTime = $end - $start;
// 每次往返包含2次切换:生产者→消费者→生产者
$avgSwitchTime = $totalTime / (SWITCH_COUNT * 2);
echo "精确协程切换测试:\n";
echo "总切换次数: " . (SWITCH_COUNT * 2) . "\n";
echo "总耗时: " . $totalTime . " ns\n";
echo "平均每次切换耗时: " . number_format($avgSwitchTime, 2) . " ns\n";
echo "相当于: " . number_format($avgSwitchTime / 1000, 2) . " μs\n";
}
accurateCoroutineSwitchTest();
为什么会发生死锁
你看到的致命错误 [FATAL ERROR]: all coroutines (count: 2) are asleep - deadlock! 是因为两个协程互相等待对方操作,导致都没有协程可以继续执行。
让我们一步步分析代码的执行流程,就能找到死锁发生的原因。
- 主协程执行
$channel->push(0):通道状态:满 (包含值0) - 主协程执行
$value = $channel->pop(): 此时通道是满的,所以pop()操作会立即成功,取出值0并赋值给$value。 通道状态:空 - 主协程因为没有被阻塞所以会循环 SWITCH_COUNT 次「主协程自己push、自己pop」。
- 当主协程的循环终于结束后,它才创建消费者协程。
- 然后主协程执行
$done->pop(),等待消费者发来结束信号。 - 消费者协程开始执行,立即运行
$data = $channel->pop()- 此时通道状态:空 (因为主协程在最后一步 pop 了)
- 因此,消费者协程的
pop()操作无法立即完成,消费者协程被挂起,等待数据。
- 此时:
- 主协程:挂起在
$done->pop(),等待消费者发信号。 - 消费者协程:挂起在
$channel->pop(),等待主协程发数据。
- 主协程:挂起在
- 两个协程都在等待对方先行动,死锁发生。
如何修改才能避免死锁?
要解决这个问题,你需要让两个协程的 push 和 pop 操作形成一个交替的、无阻塞的模式。
调换一下代码位置:先执行消费者协程pop让其阻塞等待主协程的消息,然后生产者再 push 之后等待消费者处理,这样才能形成有效的“乒乓”模式。
const SWITCH_COUNT = 100000;
function accurateCoroutineSwitchTest()
{
$start = hrtime(true);
Swoole\Coroutine\run(function () {
$channel = new Swoole\Coroutine\Channel(1);
$done = new Swoole\Coroutine\Channel(1);
// 消费者协程
go(function () use ($channel, $done) {
for ($i = 0; $i < SWITCH_COUNT; $i++) {
$data = $channel->pop();
$channel->push($data + 1);
}
$done->push(true);
});
// 生产者协程 (在主协程中运行)
$value = 0;
for ($i = 0; $i < SWITCH_COUNT; $i++) {
$channel->push($value);
$value = $channel->pop();
}
$done->pop(); // 等待消费者完成
});
$end = hrtime(true);
$totalTime = $end - $start;
// 每次往返包含2次切换:生产者→消费者→生产者
$avgSwitchTime = $totalTime / (SWITCH_COUNT * 2);
echo "精确协程切换测试:\n";
echo "总切换次数: " . (SWITCH_COUNT * 2) . "\n";
echo "总耗时: " . $totalTime . " ns\n";
echo "平均每次切换耗时: " . number_format($avgSwitchTime, 2) . " ns\n";
echo "相当于: " . number_format($avgSwitchTime / 1000, 2) . " μs\n";
}
accurateCoroutineSwitchTest();
总结与核心问题
- 顺序问题:主协程独自完成了所有次数的「push和pop」,然后才启动消费者。消费者启动时,主协程已经不再生产数据了。
- 等待依赖:主协程在等消费者发完成信号 (
$done->pop()),而消费者在等主协程发数据 ($channel->pop())。两者互相等待。
所以,原始代码的死锁错误信息:
[FATAL ERROR]: all coroutines (count: 2) are asleep - deadlock!
其含义是:
- Coroutine-1 (主协程):在
$done->pop()上睡着了。 - Coroutine-2 (消费者协程):在
$channel->pop()上睡着了。 - 没有人能唤醒对方,形成死锁。