协程死锁问题

实例

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!因为两个协程互相等待对方操作,导致都没有协程可以继续执行
让我们一步步分析代码的执行流程,就能找到死锁发生的原因。

  1. 主协程执行 $channel->push(0):通道状态: (包含值 0)
  2. 主协程执行 $value = $channel->pop()此时通道是满的,所以 pop() 操作会立即成功,取出值 0 并赋值给 $value。 通道状态:
  3. 主协程因为没有被阻塞所以会循环 SWITCH_COUNT 次「主协程自己push、自己pop」。
  4. 当主协程的循环终于结束后,它才创建消费者协程
  5. 然后主协程执行 $done->pop(),等待消费者发来结束信号。
  6. 消费者协程开始执行,立即运行 $data = $channel->pop()
    • 此时通道状态 (因为主协程在最后一步 pop 了)
    • 因此,消费者协程的 pop() 操作无法立即完成,消费者协程被挂起,等待数据。
  7. 此时:
    • 主协程:挂起在 $done->pop(),等待消费者发信号。
    • 消费者协程:挂起在 $channel->pop(),等待主协程发数据。
  8. 两个协程都在等待对方先行动,死锁发生。

如何修改才能避免死锁?

要解决这个问题,你需要让两个协程的 pushpop 操作形成一个交替的、无阻塞的模式。
调换一下代码位置:先执行消费者协程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();

总结与核心问题

  1. 顺序问题:主协程独自完成了所有次数的「push和pop」,然后才启动消费者。消费者启动时,主协程已经不再生产数据了。
  2. 等待依赖:主协程在等消费者发完成信号 ($done->pop()),而消费者在等主协程发数据 ($channel->pop())。两者互相等待

所以,原始代码的死锁错误信息:
[FATAL ERROR]: all coroutines (count: 2) are asleep - deadlock!

其含义是: