02-协程与Swoole

14 阅读3分钟

协程与 Swoole 底层原理

深入理解 Swoole 和 Hyperf 的底层实现机制

1. Swoole 底层架构

1.1 Swoole 架构图

┌─────────────────────────────────────────┐
│              Master Process             │ 主进程
│  - 监听端口                              │
│  - 管理 Worker/Task 进程                 │
│  - 信号处理                              │
└──────────────┬──────────────────────────┘
               │
       ┌───────┴───────┐
       │               │
┌──────▼──────┐  ┌────▼──────┐
│   Manager   │  │  Reactor  │ Reactor 线程组
│   Process   │  │  Threads  │ - 事件循环
│  - 进程管理  │  │  - epoll  │ - IO 多路复用
└──────┬──────┘  └───────────┘
       │
   ┌───┴────┬─────────┬─────────┐
   │        │         │         │
┌──▼───┐ ┌─▼────┐ ┌──▼────┐ ┌──▼────┐
│Worker│ │Worker│ │Worker│ │  Task │ Worker/Task 进程
│ PHP  │ │ PHP  │ │ PHP  │ │ PHP   │ - 处理业务逻辑
└──────┘ └──────┘ └──────┘ └───────┘

1.2 四种进程类型

1. Master 进程
// 主进程职责
- 创建 Reactor 线程组
- 创建 Manager 进程
- 监听端口,接收新连接
- 信号处理(reload、shutdown)
- 定时器管理
2. Manager 进程
// 管理进程职责
- 创建和管理 Worker 进程
- 创建和管理 Task 进程
- 进程监控和重启
- 进程间通信转发
3. Reactor 线程
// Reactor 线程职责
- epoll/kqueue 事件循环
- 接收客户端连接
- 接收客户端数据
- 数据发送
- 连接管理
4. Worker 进程
// Worker 进程职责
- 处理业务逻辑
- 协程调度
- 处理定时器
- 可选:处理异步任务
5. Task 进程
// Task 进程职责
- 处理耗时任务
- 同步阻塞任务
- 不支持协程

2. Swoole 进程通信方式

2.1 Unix Socket(默认方式)

// Swoole 默认使用 Unix Socket 进行进程间通信
// 特点:
// - 高性能(内核传输)
// - 可靠(基于 TCP)
// - 自动重连

// 示例:Worker 投递任务到 Task
$server->task([
    'type' => 'send_email',
    'data' => ['to' => 'user@example.com']
]);

// 底层通信流程:
// Worker → Unix Socket → Manager → Unix Socket → Task

2.2 消息队列(sysvmsg)

// config/autoload/server.php
return [
    'mode' => SWOOLE_PROCESS,
    'settings' => [
        'task_ipc_mode' => 2,  // 使用消息队列
        'message_queue_key' => ftok(__FILE__, 1),
    ],
];

// 特点:
// - 支持队列
// - 持久化(进程重启后消息不丢失)
// - 性能略低于 Unix Socket

2.3 共享内存(Swoole Table)

// 创建共享内存表
$table = new Swoole\Table(1024);
$table->column('id', Swoole\Table::TYPE_INT);
$table->column('name', Swoole\Table::TYPE_STRING, 64);
$table->column('num', Swoole\Table::TYPE_FLOAT);
$table->create();

// 写入数据(任意进程)
$table->set('user_1', [
    'id' => 1,
    'name' => 'Alice',
    'num' => 99.5
]);

// 读取数据(任意进程)
$data = $table->get('user_1');

// 特点:
// - 零拷贝,性能极高
// - 所有进程可见
// - 数据结构固定
// - 适合存储配置、计数器等

2.4 管道(Pipe)

// Swoole 内部使用管道进行 Worker 和 Task 通信
// 不对外暴露,自动管理

// 特点:
// - 单向通信
// - 内核级传输
// - 高性能

2.5 进程通信对比

方式性能可靠性数据类型适用场景
Unix Socket任意默认推荐
消息队列任意需要持久化
共享内存极高固定结构配置、计数器
管道极高任意内部使用

3. Swoole 事件循环机制

3.1 Reactor 模型

// Swoole 使用 Reactor 模型实现事件循环
// 基于 epoll(Linux)、kqueue(macOS)

while (true) {
    // 1. 监听事件
    $events = epoll_wait($epfd, $timeout);
    
    // 2. 处理事件
    foreach ($events as $event) {
        if ($event->type == 'read') {
            // 读事件:接收数据
            $data = recv($event->fd);
            dispatch_to_worker($data);
        } elseif ($event->type == 'write') {
            // 写事件:发送数据
            send($event->fd, $buffer);
        } elseif ($event->type == 'close') {
            // 关闭事件
            close_connection($event->fd);
        }
    }
    
    // 3. 处理定时器
    check_timers();
}

3.2 事件驱动流程

客户端连接
    ↓
Reactor 监听到连接事件
    ↓
分配给 Worker 进程
    ↓
Worker 处理请求(协程)
    ↓
返回响应给 Reactor
    ↓
Reactor 发送给客户端

3.3 Swoole 定时器原理

// Swoole 定时器基于 epoll_wait 的 timeout 实现
// 使用最小堆管理定时器

// 添加定时器
$timer_id = Swoole\Timer::tick(1000, function() {
    echo "每秒执行一次\n";
});

// 底层实现
class TimerHeap {
    private $heap = [];  // 最小堆
    
    public function add($ms, $callback) {
        $expire_time = microtime(true) + $ms / 1000;
        $this->heap[] = [$expire_time, $callback];
        $this->heapify();
    }
    
    public function getNextTimeout() {
        if (empty($this->heap)) {
            return -1;  // 无限等待
        }
        $next_time = $this->heap[0][0];
        $now = microtime(true);
        return max(0, ($next_time - $now) * 1000);
    }
    
    public function tick() {
        $now = microtime(true);
        while (!empty($this->heap) && $this->heap[0][0] <= $now) {
            [$expire_time, $callback] = array_shift($this->heap);
            $callback();
        }
    }
}

4. 协程原理

4.1 协程是什么?

// 协程是一种用户态的轻量级线程
// 特点:
// 1. 轻量级:创建成本低,内存占用小
// 2. 用户态调度:无需系统调用
// 3. 协作式:主动让出 CPU
// 4. 保存上下文:可恢复执行

// 对比
线程:
  - 内核态调度
  - 抢占式
  - 创建成本高(1-2MB 栈空间)
  - 系统调用开销大

协程:
  - 用户态调度
  - 协作式
  - 创建成本低(2KB 栈空间)
  - 无系统调用开销

4.2 协程调度原理

// Swoole 协程调度器
class CoroutineScheduler {
    private $runningCoroutine = null;
    private $waitingQueue = [];
    
    public function createCoroutine($callable) {
        $coroutine = new Coroutine($callable);
        $this->schedule($coroutine);
    }
    
    public function schedule($coroutine) {
        $this->runningCoroutine = $coroutine;
        $result = $coroutine->resume();
        
        if ($result === 'IO_WAIT') {
            // IO 等待,加入等待队列
            $this->waitingQueue[] = $coroutine;
            $this->runNext();
        } elseif ($result === 'FINISHED') {
            // 协程完成
            $this->runNext();
        }
    }
    
    public function runNext() {
        if (!empty($this->waitingQueue)) {
            $next = array_shift($this->waitingQueue);
            $this->schedule($next);
        }
    }
    
    public function onIOReady($coroutine) {
        // IO 就绪,恢复协程
        $this->schedule($coroutine);
    }
}

// 示例:协程切换
go(function() {
    echo "A1\n";
    Co::sleep(1);  // 让出 CPU
    echo "A2\n";
});

go(function() {
    echo "B1\n";
    Co::sleep(1);  // 让出 CPU
    echo "B2\n";
});

// 输出:A1 B1 A2 B2

4.3 协程上下文保存

// Swoole 使用 C 语言的 ucontext 实现协程上下文切换
// 上下文包括:
// 1. CPU 寄存器状态
// 2. 栈指针
// 3. 程序计数器(PC)
// 4. 局部变量

// PHP 层面的协程上下文
use Hyperf\Context\Context;

// 设置上下文
Context::set('user_id', 123);

go(function() {
    // 每个协程有独立的上下文
    $userId = Context::get('user_id');  // 123
});

go(function() {
    // 另一个协程的上下文是隔离的
    $userId = Context::get('user_id');  // null
});

4.4 协程 Hook

// Swoole 通过 Hook 将同步阻塞函数转换为协程异步

// 启用 Hook
Co::set(['hook_flags' => SWOOLE_HOOK_ALL]);

// Hook 前(阻塞)
$ch = curl_init('http://example.com');
curl_exec($ch);  // 阻塞整个进程

// Hook 后(协程)
$ch = curl_init('http://example.com');
curl_exec($ch);  // 只阻塞当前协程,其他协程继续执行

// Swoole Hook 原理
// 1. 替换 PHP 内置函数(如 curl_exec)
// 2. 检测到 IO 操作时,注册事件到 EventLoop
// 3. 让出 CPU,切换到其他协程
// 4. IO 就绪后,恢复协程执行

// 支持 Hook 的函数
- 文件操作:fopen, fread, fwrite
- 网络操作:curl_exec, file_get_contents
- 数据库:PDO, mysqli
- Redis:redis 扩展
- 睡眠:sleep, usleep

5. Hyperf 协程机制

5.1 Hyperf 协程容器

// Hyperf 在 Swoole 协程基础上封装了协程容器
use Hyperf\Coroutine\Coroutine;

// 创建协程
Coroutine::create(function() {
    // 协程代码
});

// 并发执行
use Hyperf\Coroutine\Parallel;

$parallel = new Parallel(10);  // 最多 10 个并发

for ($i = 0; $i < 100; $i++) {
    $parallel->add(function() use ($i) {
        return httpRequest("http://api.example.com?id={$i}");
    });
}

$results = $parallel->wait();  // 等待所有协程完成

5.2 协程上下文管理

use Hyperf\Context\Context;

// 请求级上下文
// 每个请求在独立的协程中运行,拥有独立的上下文

class UserMiddleware {
    public function process($request, $handler) {
        // 从请求中获取用户信息
        $userId = $this->getUserIdFromToken($request);
        
        // 存储到协程上下文
        Context::set('user_id', $userId);
        
        return $handler->handle($request);
    }
}

class UserService {
    public function getCurrentUserId() {
        // 从协程上下文获取用户 ID
        return Context::get('user_id');
    }
}

// 原理:
// Context 内部使用 Swoole\Coroutine::getContext() 实现
// 每个协程有独立的上下文容器

5.3 协程安全

// ❌ 不安全:全局变量在协程间共享
class UnsafeService {
    private $userId;  // 全局变量
    
    public function setUserId($id) {
        $this->userId = $id;
    }
    
    public function getUserId() {
        return $this->userId;  // 可能被其他协程修改
    }
}

// ✅ 安全:使用协程上下文
class SafeService {
    public function setUserId($id) {
        Context::set('user_id', $id);
    }
    
    public function getUserId() {
        return Context::get('user_id');
    }
}

// ✅ 安全:使用协程局部变量
go(function() {
    $userId = 123;  // 协程局部变量
    Co::sleep(1);
    echo $userId;   // 始终是 123
});

6. Swoole 底层优化

6.1 零拷贝技术

// Swoole 使用 sendfile 系统调用实现零拷贝
$server->on('request', function($request, $response) {
    // 传统方式(需要 4 次拷贝)
    // 磁盘 → 内核缓冲区 → 用户空间 → 内核缓冲区 → 网卡
    $content = file_get_contents('/path/to/file.jpg');
    $response->end($content);
    
    // 零拷贝(2 次拷贝)
    // 磁盘 → 内核缓冲区 → 网卡
    $response->sendfile('/path/to/file.jpg');
});

6.2 内存池

// Swoole 使用内存池减少内存分配

// 传统方式
for ($i = 0; $i < 10000; $i++) {
    $buffer = str_repeat('a', 1024);  // 每次分配内存
    process($buffer);
    unset($buffer);  // 释放内存
}

// Swoole 内存池
// 预分配一块内存,重复使用
$pool = new Swoole\Memory\Pool(1024 * 1024);  // 1MB

for ($i = 0; $i < 10000; $i++) {
    $buffer = $pool->alloc(1024);  // 从池中分配
    process($buffer);
    $pool->free($buffer);  // 归还到池
}

6.3 EventLoop 优化

// Swoole 对 epoll 进行了优化

// 1. ET 模式(Edge Triggered)
// 只在状态变化时触发,性能更高

// 2. 连接预分配
// 预先分配连接对象,避免频繁创建

// 3. Buffer 复用
// 读写 Buffer 复用,减少内存分配

7. 性能对比

7.1 并发模型对比

// PHP-FPM(同步阻塞)
// 每个请求占用一个进程
// 100 个并发请求 = 100 个进程
// 内存占用:100 * 30MB = 3GB

// Swoole(协程异步)
// 每个请求占用一个协程
// 100 个并发请求 = 100 个协程
// 内存占用:100 * 2KB = 200KB

// 性能提升:10-100 倍

7.2 QPS 对比

场景:API 接口,查询数据库并返回 JSON

PHP-FPM:
  - QPS: 500-1000
  - 响应时间: 100ms
  - 资源占用: 高

Hyperf (Swoole):
  - QPS: 10000-50000
  - 响应时间: 10ms
  - 资源占用: 低

8. 高频问题

Q1: Swoole 的进程模型是怎样的?

答案: Swoole 采用多进程 + Reactor 线程模型:

  • Master 进程:主进程,负责监听端口和创建子进程
  • Manager 进程:管理进程,负责管理 Worker 和 Task 进程
  • Reactor 线程:事件循环线程,负责处理网络 IO
  • Worker 进程:工作进程,处理业务逻辑
  • Task 进程:任务进程,处理异步任务

优势:

  • 进程隔离,稳定性高
  • 充分利用多核 CPU
  • Reactor 线程处理网络 IO,Worker 处理业务,分工明确

Q2: Swoole 进程间如何通信?

答案: Swoole 支持多种进程间通信方式:

  1. Unix Socket(默认):高性能、可靠
  2. 消息队列:支持持久化
  3. 共享内存(Table):零拷贝,极高性能
  4. 管道:内部使用

实际应用:

  • Worker → Task:Unix Socket 投递任务
  • 进程间共享数据:Swoole\Table
  • 配置热更新:信号 + 共享内存

Q3: 协程和线程有什么区别?

答案

特性线程协程
调度内核态(抢占式)用户态(协作式)
切换成本高(系统调用)低(用户态)
内存占用1-2MB2KB
并发数量数千数十万
适用场景CPU 密集IO 密集

协程优势:

  • 轻量级,可创建大量协程
  • 切换快,无系统调用开销
  • 适合高并发 IO 场景

Q4: Swoole Hook 的原理是什么?

答案: Swoole Hook 通过替换 PHP 内置函数实现同步转异步:

  1. 替换函数:Hook 后,curl_exec 等函数被 Swoole 重写
  2. 检测 IO:检测到 IO 操作时,注册事件到 EventLoop
  3. 协程切换:让出 CPU,切换到其他协程
  4. IO 就绪:EventLoop 通知 IO 就绪,恢复协程

支持的函数:文件 IO、网络 IO、数据库、Redis 等

Q5: 如何避免协程安全问题?

答案

  1. 使用协程上下文:Context::set/get
  2. 避免全局变量:使用依赖注入
  3. 使用协程锁:Channel、WaitGroup
  4. 一次性容器:每个协程创建独立实例
// ❌ 不安全
class Service {
    private $cache = [];
    
    public function get($key) {
        return $this->cache[$key] ?? null;
    }
}

// ✅ 安全
class Service {
    public function get($key) {
        return Context::get("cache.{$key}");
    }
}

9. 总结

Swoole 和 Hyperf 核心原理:

  1. 进程模型:Master + Manager + Reactor + Worker
  2. 进程通信:Unix Socket、共享内存、消息队列
  3. 事件循环:Reactor + epoll
  4. 协程调度:用户态、协作式、上下文保存
  5. 协程 Hook:同步转异步
  6. 性能优化:零拷贝、内存池、连接复用

重点

  • 能说出 Swoole 的进程模型和通信方式
  • 理解协程的原理和优势
  • 知道如何避免协程安全问题
  • 能举例说明 Swoole 的性能优势

推荐学习