协程与 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 支持多种进程间通信方式:
- Unix Socket(默认):高性能、可靠
- 消息队列:支持持久化
- 共享内存(Table):零拷贝,极高性能
- 管道:内部使用
实际应用:
- Worker → Task:Unix Socket 投递任务
- 进程间共享数据:Swoole\Table
- 配置热更新:信号 + 共享内存
Q3: 协程和线程有什么区别?
答案:
| 特性 | 线程 | 协程 |
|---|---|---|
| 调度 | 内核态(抢占式) | 用户态(协作式) |
| 切换成本 | 高(系统调用) | 低(用户态) |
| 内存占用 | 1-2MB | 2KB |
| 并发数量 | 数千 | 数十万 |
| 适用场景 | CPU 密集 | IO 密集 |
协程优势:
- 轻量级,可创建大量协程
- 切换快,无系统调用开销
- 适合高并发 IO 场景
Q4: Swoole Hook 的原理是什么?
答案: Swoole Hook 通过替换 PHP 内置函数实现同步转异步:
- 替换函数:Hook 后,curl_exec 等函数被 Swoole 重写
- 检测 IO:检测到 IO 操作时,注册事件到 EventLoop
- 协程切换:让出 CPU,切换到其他协程
- IO 就绪:EventLoop 通知 IO 就绪,恢复协程
支持的函数:文件 IO、网络 IO、数据库、Redis 等
Q5: 如何避免协程安全问题?
答案:
- 使用协程上下文:Context::set/get
- 避免全局变量:使用依赖注入
- 使用协程锁:Channel、WaitGroup
- 一次性容器:每个协程创建独立实例
// ❌ 不安全
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 核心原理:
- ✅ 进程模型:Master + Manager + Reactor + Worker
- ✅ 进程通信:Unix Socket、共享内存、消息队列
- ✅ 事件循环:Reactor + epoll
- ✅ 协程调度:用户态、协作式、上下文保存
- ✅ 协程 Hook:同步转异步
- ✅ 性能优化:零拷贝、内存池、连接复用
重点:
- 能说出 Swoole 的进程模型和通信方式
- 理解协程的原理和优势
- 知道如何避免协程安全问题
- 能举例说明 Swoole 的性能优势
推荐学习:
- 阅读 Swoole 文档:wiki.swoole.com/
- 阅读 Hyperf 文档:hyperf.wiki/
- 实践:搭建 Hyperf 项目,体验协程