协程基础

概念说明

Viswoole 框架基于 Swoole 协程 提供高性能的并发编程能力。协程(Coroutine)是轻量级的用户态线程,可以在单线程内实现并发执行,避免了传统多线程模型中的上下文切换开销。

框架的 Coroutine 类对 Swoole 协程进行了封装和扩展,提供以下能力:

方法说明
getContext()获取当前协程上下文
isCoroutine()判断当前是否处于协程环境
id()获取当前协程 ID
getTopId()获取当前顶级协程 ID

工作原理

协程与非协程环境兼容

框架同时支持协程和非协程两种运行环境:

  • 协程环境(Swoole Server):直接使用 Swoole 原生协程能力
  • 非协程环境(CLI / PHPUnit 测试等):通过 mock_context 提供兼容层,确保代码在不同环境下行为一致

这种设计使得业务代码无需关心底层是否运行在协程环境中。

协程上下文隔离

这是框架协程设计的核心机制。容器(Container)在协程环境中按 顶级协程 ID 隔离单例:

text
协程 #1 (TopId: 1)
  ├── Container 单例 A (独立状态)
  ├── Database 连接池 A
  └── Cache 实例 A

协程 #2 (TopId: 2)
  ├── Container 单例 B (独立状态)
  ├── Database 连接池 B
  └── Cache 实例 B

每个顶级协程拥有完全隔离的服务实例集合,彻底避免并发状态竞争问题。

顶级协程的概念

Swoole 协程存在父子关系,子协程继承父协程的上下文。框架使用 顶级协程 ID(即协程树的根节点 ID)作为隔离键,确保同一个请求链路上的所有子协程共享同一组单例。

代码示例

检测当前环境

php
use Viswoole\Core\Coroutine;

if (Coroutine::isCoroutine()) {
    echo "当前运行在协程环境中";
    echo "协程 ID: " . Coroutine::id();
    echo "顶级协程 ID: " . Coroutine::getTopId();
} else {
    echo "当前运行在非协程环境中";
}

协程上下文的使用

php
use Viswoole\Core\Coroutine;

// 获取当前协程上下文(自动处理非协程环境的兼容)
$context = Coroutine::getContext();

// 在上下文中存取数据(仅当前协程可见)
$context['request_id'] = uniqid();
$context['user_id'] = 1001;

// 后续在同一协程中可读取
$requestId = Coroutine::getContext()['request_id'];

容器在协程中的行为

php
// 在不同协程中,同名的 singleton 绑定返回不同的实例

// 协程 A 中
go(function () {
    $dbA = app()->make(Database::class);
    $dbA->setConnection('conn_A');
});

// 协程 B 中
go(function () {
    $dbB = app()->make(Database::class);
    // $dbB !== $dbA,两者完全独立
    $dbB->getConnection(); // 不会受到协程 A 的影响
});

创建协程

php
use Swoole\Coroutine;

// 创建新协程
Coroutine::create(function () {
    // 此代码在独立协程中执行
    echo "协程 ID: " . Coroutine::getCid();

    // 可以嵌套创建子协程
    Coroutine::create(function () {
        // 子协程,getTopId() 与父协程相同
        echo "顶级协程 ID: " . \Viswoole\Core\Coroutine::getTopId();
    });
});

echo "主协程继续执行";

最佳实践

1. 不要阻塞协程

协程是协作式调度,阻塞操作会挂起当前协程并影响整体性能:

php
// ❌ 不推荐:sleep() 会阻塞整个进程
Coroutine::create(function () {
    sleep(5); // 阻塞 5 秒,期间无法处理其他请求
});

// ✅ 推荐:使用 Swoole 的异步睡眠
Coroutine::create(function () {
    Swoole\Coroutine::sleep(5); // 只挂起当前协程,不影响其他协程
});

2. 理解协程安全的边界

协程上下文隔离保证了框架层面的安全性,但仍需注意共享资源的访问:

php
// ✅ 安全:容器管理的单例自动隔离
$db = app()->make(Database::class); // 每个协程独立实例

// ⚠️ 注意:全局变量、静态属性、文件系统不受协程隔离保护
class Counter
{
    private static int $count = 0; // 所有协程共享!

    public static function increment(): int
    {
        return ++self::$count; // 并发不安全
    }
}

// 如需跨协程共享计数,使用 Swoole\Atomic 或 Redis 等线程安全方案

3. 合理利用协程上下文传递数据

利用协程上下文在请求链路中透传数据,避免参数层层传递:

php
// 中间件中设置
Coroutine::getContext()['trace_id'] = generateTraceId();
Coroutine::getContext()['user'] = $currentUser;

// Controller / Service 中直接读取
$traceId = Coroutine::getContext()['trace_id'];

// 日志中间件自动采集
Log::withContext([
    'trace_id' => Coroutine::getContext()['trace_id'],
    'coroutine_id' => Coroutine::id(),
]);

4. 编写协程兼容的代码

确保代码在协程和非协程环境中都能正常工作:

php
class DataService
{
    public function process(): void
    {
        // ✅ 使用框架提供的 Coroutine 封装,自动兼容两种环境
        $context = \Viswoole\Core\Coroutine::getContext();

        if ($context !== null) {
            $context['start_time'] = microtime(true);
        }

        // 业务逻辑...
    }
}