1. 项目概述从“阻塞”到“并发”的思维跃迁“PHP的协程支持”这个话题对于很多习惯了传统同步阻塞编程的PHP开发者来说既熟悉又陌生。熟悉的是我们每天都在用PHP处理Web请求、操作数据库、调用API陌生的是当我们需要处理高并发I/O密集型任务时比如同时请求十个外部接口、处理大量文件上传、或者构建一个实时聊天服务传统的“一个请求一个进程/线程”的模型就显得力不从心资源消耗巨大响应速度也上不去。这时协程Coroutine作为一种轻量级的用户态线程就成了破局的关键。它允许我们在一个线程内实现多个任务的切换在等待I/O如网络响应、磁盘读写时主动让出CPU去执行其他任务从而极大地提升程序的并发能力和资源利用率。简单来说你可以把传统的PHP-FPM模式想象成一个餐馆每个顾客请求都需要一个专属的服务员进程。即使顾客在点菜后发呆等待上菜即I/O阻塞这个服务员也只能干等着不能去服务其他顾客。而协程模式则像是一个超级服务员他可以同时服务多桌顾客。当A桌顾客在等菜时他立刻去B桌点单B桌等菜时他又去C桌结账。通过这种高效的“时间管理”一个服务员就能干好几个服务员的活餐馆服务器的接待能力自然大幅提升。近年来随着Swoole、Workerman等扩展的成熟以及PHP 8.1原生纤程Fiber的引入PHP的协程生态已经非常完善。无论是想用Swoole构建高性能微服务、用ReactPHP处理异步事件流还是仅仅想优化自己脚本中的批量HTTP请求理解并应用协程都已成为现代PHP开发者的一项核心技能。这篇文章我将从一个老码农的视角带你彻底搞懂PHP协程的来龙去脉、核心原理、主流方案选型并通过大量接地气的实操案例让你不仅能理解概念更能真正把它用起来解决实际开发中的性能瓶颈。2. 核心原理协程到底是什么以及它如何工作要理解协程我们必须先把它和几个容易混淆的概念——进程、线程、回调、Promise/Async-Await——区分清楚。很多人一上来就扎进Swoole的代码里结果越看越迷糊根本原因就是底层概念没打通。2.1 进程、线程与协程的层级关系想象一下你的电脑是一个大工厂。进程就像是工厂里一个个独立的生产车间。每个车间进程有自己独立的空间内存地址空间、原料和工具系统资源车间之间通常需要复杂的流程进程间通信IPC才能传递货物。PHP-FPM模式下每个请求通常就对应一个独立的PHP进程或线程池中的线程。创建和销毁车间开销巨大。线程是车间里的工人。同一个车间进程里的工人们线程共享车间的所有空间和资源协同工作。他们之间沟通容易共享内存但管理不当容易打架线程安全问题需要加锁。一个进程可以有多个线程这就是多线程模型。协程则可以看作是一个工人掌握的“多任务处理技能”。这个工人线程可以在任务A做到一半比如去仓库取料这是I/O等待时把当前状态记下来然后立刻切换到任务B继续做。等仓库通知料到了他又能切回任务A从刚才记下的状态继续。关键在于这一切切换是由工人程序自己主动控制的而不是由车间主任操作系统强行打断调度。因此协程是用户态的切换代价极低可能一个工厂里只有一个车间、一个工人但这个工人却能通过协程技能同时处理成千上万个任务。对于PHP而言在传统的Web服务模型中Apache mod_php 或 Nginx PHP-FPM每个HTTP请求都会映射到一个独立的PHP进程或线程。这个进程从头到尾同步执行你的代码遇到file_get_contents()、curl_exec()或mysqli_query()这样的阻塞函数时整个进程就“挂起”等待什么也做不了直到I/O完成。这就是性能瓶颈的根源。2.2 协程的核心保存与恢复上下文协程之所以能“暂停”和“继续”核心在于它能保存和恢复“执行上下文”。这个上下文包括栈信息当前函数的调用栈、局部变量等。寄存器状态程序计数器的值执行到哪一行代码了等。当协程遇到I/O操作主动调用yield让出或resume恢复时当前的上下文被保存起来然后加载另一个协程的上下文并执行。I/O操作在后台通过事件循环Event Loop进行监听一旦完成就会通知事件循环事件循环再安排对应的协程在合适的时候恢复执行。这个过程完全在PHP虚拟机内部完成不涉及操作系统层面的进程/线程切换因此开销极小。你可以轻松创建数万甚至数十万个协程而创建同样数量的进程或线程是不可想象的。2.3 与回调、Promise/Async-Await的关系在协程普及之前PHP处理异步主要靠回调地狱Callback Hell。代码逻辑被拆散到一个个回调函数中可读性和可维护性极差。// 传统的回调风格伪代码例如ReactPHP $http-get(url1, function ($response) { // 处理第一个响应 $http-get(url2, function ($response) { // 处理第二个响应嵌套更深... }); });后来引入了Promise和Async/Await语法糖。Promise代表一个未来值而Async/Await让我们可以用近乎同步的代码风格来写异步逻辑。PHP的协程正是通过生成器Generator和yield关键字或者通过Swoole的协程API或者通过PHP 8.1的Fiber实现了Async/Await的底层机制。// 使用协程如Swoole的同步写法风格 go(function () { $result1 Co::exec(curl http://api1.com); // 协程化curl遇到网络IO自动切换 $result2 Co::exec(curl http://api2.com); // 处理 $result1 和 $result2 });看起来是同步的代码但Co::exec内部在发起网络请求后当前协程会挂起让出CPU给其他协程等网络数据返回后再恢复执行。这就是协程的魅力编写的是符合人类直觉的同步代码享受的却是异步非阻塞的高性能。3. 主流方案选型Swoole、ReactPHP与原生FiberPHP生态中有多个协程/异步解决方案各有侧重选择哪个取决于你的具体场景和技术栈。3.1 Swoole企业级全栈协程框架Swoole是一个使用C语言编写的PHP扩展它不仅仅是协程更是一个网络通信引擎和并发框架。它从底层重构了PHP的网络I/O、进程管理、内存管理等模块。核心特点协程化客户端内置了协程版本的MySQL、Redis、HTTP、WebSocket等客户端你几乎可以无感地将阻塞调用替换为协程调用。常驻内存Swoole服务是常驻内存的避免了传统PHP每次请求都要经历“初始化-执行-销毁”的开销性能提升一个数量级。全栈解决方案提供了HttpServer、WebSocketServer、TCP/UDP Server甚至内置了进程管理、定时器、连接池、表内存等高级特性。生态强大基于Swoole有Hyperf、EasySwoole、IMI等优秀的框架可以快速构建微服务、游戏服务器、物联网网关等。适用场景需要构建独立高性能网络服务API网关、微服务、长连接推送、游戏后端。需要对现有PHP项目进行彻底的性能改造处理高并发I/O。团队有C/C背景能接受学习曲线稍陡峭的扩展。一个简单的Swoole HTTP服务器示例// server.php $http new Swoole\Http\Server(0.0.0.0, 9501); $http-on(request, function ($request, $response) { // 在这里面你可以使用协程 go(function() use ($request, $response) { // 协程内进行MySQL查询使用协程客户端 $mysql new Swoole\Coroutine\MySQL(); $mysql-connect([host 127.0.0.1, user root, password , database test]); $result $mysql-query(SELECT * FROM users LIMIT 10); // 此查询不会阻塞进程 // 同时并发请求一个外部API $httpClient new Swoole\Coroutine\Http\Client(api.example.com, 443, true); $httpClient-get(/data); $apiResult $httpClient-body; $response-header(Content-Type, application/json); $response-end(json_encode([db $result, api $apiResult])); }); }); $http-start();运行php server.php你就得到了一个高性能的协程HTTP服务器。注意Swoole的协程环境有严格限制。协程切换只能发生在特定的“协程化”函数调用中如协程Client的操作、Co::sleep等。在协程内禁止使用会导致进程阻塞的函数如原生的sleep()、mysqli_query、file_get_contents非协程版等否则会阻塞整个事件循环。务必使用Swoole提供的协程替代品。3.2 ReactPHP基于事件循环的纯PHP库ReactPHP是一个用纯PHP编写的事件驱动、非阻塞I/O库。它的核心是一个事件循环Event Loop所有异步操作都基于Promise。核心特点纯PHP实现无需安装C扩展通过Composer即可引入部署方便。Promise规范严格遵循Promises/A规范与JavaScript的异步生态概念相通。组件化提供事件循环、流、HTTP客户端/服务器、DNS、Socket等独立组件可按需组合。学习曲线需要理解事件循环和Promise链式编程对于习惯了同步思维的人有一定挑战。适用场景项目无法或不想安装PHP扩展如共享主机环境。需要处理复杂的异步事件流、构建CLI工具或守护进程。团队熟悉JavaScript的Promise/Async-Await概念迁移成本低。一个ReactPHP并发HTTP请求的例子// composer require react/http react/promise require vendor/autoload.php; use React\Http\Browser; use React\EventLoop\Loop; $loop Loop::get(); $browser new Browser($loop); $urls [http://httpbin.org/delay/1, http://httpbin.org/delay/2, http://httpbin.org/delay/3]; $promises []; foreach ($urls as $url) { $promises[] $browser-get($url)-then( function (Psr\Http\Message\ResponseInterface $response) use ($url) { echo Fetched . $url . : . strlen($response-getBody()) . bytes\n; return $url; // 可以返回结果用于后续处理 }, function (Exception $e) use ($url) { echo Error fetching . $url . : . $e-getMessage() . \n; } ); } // 等待所有请求完成 \React\Promise\all($promises)-then(function ($results) { echo All requests completed!\n; print_r($results); }); $loop-run(); // 启动事件循环3.3 PHP 8.1 原生纤程FiberPHP 8.1 引入了Fiber作为语言级别的纤程即协程支持。它提供了最底层的、不依赖任何扩展的协程原语。核心特点语言内置无需额外扩展是PHP语言的一部分。底层原语只提供Fiber::suspend()和Fiber::resume()等基本操作不包含任何事件循环或I/O调度功能。你需要自己或在其他库如ReactPHP的Fiber适配器的基础上构建完整的异步生态。灵活性高为上层异步框架如Amphp、ReactPHP的未来版本提供了统一的基础。适用场景希望使用未来标准的、无扩展依赖的协程方案。需要开发底层的异步库或框架。当前阶段直接用于生产级业务开发还比较原始通常配合其他库使用。一个简单的Fiber示例$fiber new Fiber(function (): void { echo Fiber starts\n; $value Fiber::suspend(suspended); // 挂起纤程并向外传递suspended echo Fiber resumed with value: , $value, \n; }); echo Main starts\n; $suspendedValue $fiber-start(); // 启动纤程获取其挂起时传递的值 echo Fiber suspended with value: , $suspendedValue, \n; $fiber-resume(resume); // 恢复纤程并传入值resume echo Main ends\n; // 输出 // Main starts // Fiber starts // Fiber suspended with value: suspended // Fiber resumed with value: resume // Main ends方案对比与选型建议特性SwooleReactPHPPHP Fiber实现方式C扩展性能极高纯PHP库语言内置原语性能★★★★★★★★☆☆★★★★☆ (需结合框架)学习成本较高中等高底层生态完整性非常完整客户端、服务器、连接池组件化生态较好非常原始依赖上层框架部署难度需安装扩展Composer安装简单PHP 8.1无需额外安装最佳场景高性能网络服务、微服务、彻底重构异步CLI工具、事件处理、无法装扩展的环境未来标准、底层库开发个人建议对于全新的、追求极致性能的网络服务项目首选Swoole并考虑使用基于Swoole的框架如Hyperf。对于现有项目引入异步处理或环境受限无法安装扩展ReactPHP是一个稳健的选择。PHP Fiber目前更适合关注前沿技术的开发者或库作者等待其生态成熟。如果只是想优化脚本中的批量HTTP请求或数据库查询可以单独引入Swoole的协程运行时通过--enable-swoole或使用Guzzle的Swoole Handler无需重构整个项目。4. 深入Swoole协程实战从并发HTTP到数据库连接池理论说再多不如一行代码。我们以最流行的Swoole为例深入几个核心的实战场景看看协程如何解决实际问题。我假设你已经安装了Swoole扩展pecl install swoole。4.1 场景一并发请求多个外部API这是协程最经典的用例。假设你需要从三个不同的数据源获取信息然后汇总。传统同步阻塞方式$start microtime(true); $result1 file_get_contents(http://slow-api.com/delay/3); // 假设耗时3秒 $result2 file_get_contents(http://slow-api.com/delay/2); // 假设耗时2秒 $result3 file_get_contents(http://slow-api.com/delay/1); // 假设耗时1秒 $end microtime(true); echo Total time: . ($end - $start) . seconds\n; // 总耗时约 321 6秒Swoole协程方式Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL); // 一键协程化使部分原生PHP函数变成协程友好的 $start microtime(true); go(function () { $http new Swoole\Coroutine\Http\Client(slow-api.com, 80); $http-get(/delay/3); echo Result 1 received\n; }); go(function () { $http new Swoole\Coroutine\Http\Client(slow-api.com, 80); $http-get(/delay/2); echo Result 2 received\n; }); go(function () { $http new Swoole\Coroutine\Http\Client(slow-api.com, 80); $http-get(/delay/1); echo Result 3 received\n; }); Swoole\Event::wait(); // 等待所有协程执行完毕 $end microtime(true); echo Total time: . ($end - $start) . seconds\n; // 总耗时约 max(3,2,1) 3秒通过go()函数创建协程三个HTTP请求几乎同时发起。最慢的那个决定了总耗时从6秒缩短到了约3秒性能提升一倍。实操心得Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL)这行代码非常强大它通过“钩子”将一些常用的阻塞式PHP函数如sleep,usleep,stream_socket_client,mysqli_connect等在协程环境下转化为异步非阻塞。但要注意它并非万能对于某些复杂的扩展或资源类型可能不生效。最稳妥的方式还是直接使用Swoole提供的协程客户端如Swoole\Coroutine\Http\Client。4.2 场景二协程化MySQL查询与连接池数据库查询是Web应用中最常见的阻塞操作之一。直接使用协程客户端可以避免查询阻塞整个进程。基本协程MySQL查询go(function () { $swoole_mysql new Swoole\Coroutine\MySQL(); // 连接操作本身也是协程化的不会阻塞 $connected $swoole_mysql-connect([ host 127.0.0.1, port 3306, user user, password pass, database test, ]); if (!$connected) { echo Connect failed\n; return; } // 执行查询等待结果期间协程挂起CPU执行其他任务 $result $swoole_mysql-query(SELECT * FROM large_table LIMIT 1000); if ($result false) { echo Query failed: {$swoole_mysql-error}\n; } else { echo Fetched . count($result) . rows\n; } });然而每次查询都创建新连接是低效的。在高并发下我们需要连接池。使用连接池以Hyperf的Database组件为例其底层基于Swoole协程// 配置连接池 (通常在config/autoload/databases.php) return [ default [ driver mysql, host localhost, database hyperf, username root, password , charset utf8mb4, collation utf8mb4_unicode_ci, prefix , pool [ // 连接池配置 min_connections 1, max_connections 10, connect_timeout 10.0, wait_timeout 3.0, heartbeat -1, max_idle_time 60.0, ], ], ]; // 在代码中使用框架会自动从池中获取和归还连接 $users Db::table(users)-where(active, 1)-get();连接池维护一定数量的数据库连接协程需要时从池中借用用完后归还避免了频繁创建和销毁连接的开销这是协程应用中的关键优化点。4.3 场景三构建一个简单的协程WebSocket服务器WebSocket是长连接非常适合用协程来处理大量并发连接。$server new Swoole\WebSocket\Server(0.0.0.0, 9502); // 监听WebSocket连接打开事件 $server-on(open, function (Swoole\WebSocket\Server $server, Swoole\Http\Request $request) { echo connection open: {$request-fd}\n; // 可以将$fd存入Redis或Swoole Table用于广播等操作 }); // 监听WebSocket消息事件 $server-on(message, function (Swoole\WebSocket\Server $server, Swoole\WebSocket\Frame $frame) { echo received message: {$frame-data} from {$frame-fd}\n; // 处理消息这里可以创建协程进行耗时操作 go(function() use ($server, $frame) { // 模拟耗时处理 Co::sleep(1); // 回复消息 $server-push($frame-fd, Server: Hello, you said {$frame-data}); }); // 注意即使上面创建了协程处理当前message回调也会立刻结束不会阻塞 }); // 监听连接关闭事件 $server-on(close, function ($server, $fd) { echo connection close: {$fd}\n; }); echo WebSocket server started at ws://0.0.0.0:9502\n; $server-start();这个服务器可以轻松维持成千上万个WebSocket连接。每个on(message)回调里我们都可以用go()开启协程来处理业务逻辑比如查询数据库、调用其他服务而不会阻塞其他连接的收发。5. 避坑指南与高级技巧协程带来了高性能也带来了新的编程范式和陷阱。下面是我在实际项目中踩过的一些坑和总结的经验。5.1 协程安全与上下文管理这是新手最容易出错的地方。在协程中禁止使用全局变量或静态变量来保存与单个请求相关的状态因为多个协程是交错执行的会相互污染数据。错误示例$globalUserId null; // 全局变量 go(function () { global $globalUserId; $globalUserId 100; Co::sleep(0.1); // 协程切换 echo Coroutine 1: UserId is . $globalUserId . \n; // 可能被其他协程修改 }); go(function () { global $globalUserId; $globalUserId 200; Co::sleep(0.05); echo Coroutine 2: UserId is . $globalUserId . \n; });输出结果是不可预测的。正确做法使用协程上下文Coroutine ContextSwoole提供了Co::getContext()来为每个协程创建一个独立的存储空间。go(function () { $ctx Co::getContext(); $ctx[user_id] 100; Co::sleep(0.1); echo Coroutine 1: UserId is . $ctx[user_id] . \n; // 安全始终是100 }); go(function () { $ctx Co::getContext(); // 获取的是另一个协程的上下文 $ctx[user_id] 200; Co::sleep(0.05); echo Coroutine 2: UserId is . $ctx[user_id] . \n; // 安全始终是200 });在Hyperf等框架中通常通过依赖注入和AOP来透明地管理请求级上下文开发者无需直接操作Co::getContext()。5.2 协程间通信Channel与WaitGroup协程是并发执行的如何让它们协作Swoole提供了两个核心原语Channel和WaitGroup。Channel通道类似于Golang的chan用于协程间传递数据是生产-消费者模型的完美实现。$chan new Swoole\Coroutine\Channel(10); // 创建容量为10的通道 go(function () use ($chan) { Co::sleep(1); $chan-push([data from coroutine A]); // 生产数据 }); go(function () use ($chan) { $data $chan-pop(); // 消费数据如果通道为空此协程会挂起等待 echo Received: . json_encode($data) . \n; });WaitGroup用于等待一组协程全部执行完毕。$wg new Swoole\Coroutine\WaitGroup(); $results []; for ($i 0; $i 5; $i) { $wg-add(); // 计数器1 go(function () use ($wg, $i, $results) { defer(function () use ($wg) { $wg-done(); // 协程结束时计数器-1 (defer确保执行) }); Co::sleep(rand(1, 3)); $results[$i] Result $i; }); } $wg-wait(); // 阻塞直到计数器归零所有协程完成 print_r($results); // 此时所有结果都已就绪defer是Swoole提供的特性类似于Go的defer用于注册一个在协程退出时无论正常还是异常一定会执行的函数常用于资源清理和WaitGroup::done()调用。5.3 异常处理与超时控制在协程中异常处理尤为重要。一个未捕获的异常会导致整个协程退出但不会影响其他协程。为协程设置全局异常处理器Swoole\Coroutine::set([enable_deadlock_check true]); // 开启死锁检测 // 在创建协程时最好用try-catch包裹 go(function () { try { $mysql new Swoole\Coroutine\MySQL(); if (!$mysql-connect(...)) { throw new RuntimeException(Connect failed); } // ... 业务逻辑 } catch (Throwable $e) { // 记录日志不要简单echo error_log(Coroutine error: . $e-getMessage()); // 根据业务决定是否向上抛出或进行恢复操作 } });为协程操作设置超时网络I/O必须设置超时否则协程可能永远挂起。// 方法1使用协程客户端自带的timeout选项 $client new Swoole\Coroutine\Http\Client(example.com, 80); $client-set([timeout 3.0]); // 设置3秒超时 $client-get(/); // 方法2使用Swoole的协程超时API (更灵活) $result Swoole\Coroutine::select([$socket], [], [], 5.0); // 等待socket可读超时5秒 if (empty($result)) { echo Timeout!\n; } // 方法3使用channel和timer实现带超时的pop $chan new Swoole\Coroutine\Channel(1); $timeout 2.0; Swoole\Timer::after($timeout * 1000, function () use ($chan) { if (!$chan-isClosed()) { $chan-push(false); // 超时后推送一个false } }); go(function () use ($chan) { Co::sleep(3); // 模拟一个耗时3秒的操作 if (!$chan-isClosed()) { $chan-push(true); // 正常完成 } }); $data $chan-pop(); if ($data false) { echo Operation timeout after {$timeout}s\n; }5.4 与传统PHP框架如Laravel、ThinkPHP的融合很多人想把Swoole协程的高性能引入到现有的Laravel或ThinkPHP项目中。直接替换Web服务器用Swoole HTTP Server替代NginxPHP-FPM是行不通的因为传统框架在设计上是“请求即毁”的存在大量的静态变量、单例和全局状态在常驻内存的Swoole中会导致内存泄漏和状态污染。正确的融合姿势作为独立服务将高性能、高并发的部分如WebSocket、实时推送、消息队列消费者用纯Swoole或基于Swoole的微框架如Hyperf重写作为独立服务部署。传统框架项目通过HTTP、RPC等方式与之通信。使用协程运行时优化局部在传统框架的CLI命令、队列任务处理器等脚本中通过Swoole\Runtime::enableCoroutine()来优化其中的并发I/O操作如批量发送邮件、处理日志。使用桥接包有一些社区包如laravel-s、swoole-tw尝试将Swoole作为Laravel/ThinkPHP的服务器。务必谨慎评估它们通过在每个请求结束后重置框架容器来模拟“请求隔离”能带来一定的性能提升但也可能引入一些兼容性问题需要充分测试。逐步迁移对于大型项目最稳妥的方式是逐步将模块拆分为基于Swoole的微服务最终让传统框架退化为一个前端展示层或管理后台。6. 性能调优与监控上了协程性能并非一劳永逸。不当的使用反而会带来问题。6.1 协程数量与内存虽然协程很轻量初始栈大小约2KB但创建数十万个协程仍然会消耗可观的内存。务必根据业务压力和服务器资源设置合理的协程数量上限。Swoole Server可以通过server-set([max_coroutine 100000])来配置。同时要确保协程能够正常结束避免协程泄漏即协程创建后永不退出。6.2 连接池配置对于数据库、Redis等必须使用连接池。池的大小min_connections,max_connections需要根据QPS和单个查询耗时来调整。一个简单的估算公式最大连接数 ≈ QPS * 平均查询耗时(秒)。设置过小会导致协程等待连接过大则浪费资源。6.3 使用Swoole Tracker进行监控Swoole官方提供了Swoole Tracker这个强大的监控工具可以可视化地查看所有协程的状态、创建栈、耗时。每个协程内的阻塞调用、文件I/O、网络I/O。内存泄漏检测、死锁检测。系统调用追踪。这对于定位协程阻塞、内存增长、慢请求等问题至关重要。在生产环境部署Swoole服务强烈建议搭配Tracker。6.4 避免在协程中使用阻塞函数这一点再怎么强调都不为过。反复检查你的代码确保在协程环境中使用Co::sleep()代替sleep()。使用协程MySQL/Redis客户端代替mysqli/pdo/redis扩展。使用Swoole\Coroutine\System::readFile()或Swoole\Coroutine\System::exec()代替file_get_contents()、shell_exec()。或者使用Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL | SWOOLE_HOOK_NATIVE_CURL)来尝试协程化原生函数注意覆盖范围。7. 总结与展望从我第一次接触Swoole协程时的小心翼翼到如今在多个生产项目中放心使用这个过程让我深刻体会到PHP协程带来的不仅是性能的数量级提升更是一种编程思维的转变——从面向过程的同步思维转向事件驱动、并发的异步思维。这要求我们更仔细地管理状态更谨慎地处理资源但回报是应用吞吐量和响应能力的质的飞跃。对于个人开发者和小团队从ReactPHP入手理解事件循环和Promise是一个平滑的起点。对于追求极致性能和企业级特性的项目Swoole及其生态是不二之选。而PHP Fiber作为语言未来的基石值得我们持续关注。最后再分享一个我自己的小技巧在将任何阻塞代码迁移到协程环境前先用一个简单的脚本测试其协程化后的行为。例如写一个脚本并发起10个协程化的HTTP请求到本地一个会sleep(2)的测试接口观察总耗时是接近2秒还是20秒。这个简单的测试能立刻让你直观地感受到协程的威力也能提前发现一些环境或代码上的兼容性问题。