ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Hyperf 协程详解:从 Swoole 原理到实战编程指南

Hyperf 协程详解:从 Swoole 原理到实战编程指南 后端微服务【免费下载链接】hyperf A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.项目地址https://gitcode.com/gh_mirrors/hy/hyperf点击查看免费下载本文基于 Hyperf 官方文档 docs/en/coroutine.md 与hyperf/coroutine组件源码编写深入讲解 Hyperf 协程的底层原理、编程注意事项与常用工具类帮助你真正理解并在项目中正确使用协程。Hyperf 是一个基于Swoole 5协程构建的常驻内存框架协程正是其高性能的基石之一。本文将从传统PHP-FPM的阻塞模型讲起解释协程为什么能解决高并发问题随后详细介绍协程的创建、调度、通信与上下文管理并给出hyperf/coroutine组件中Parallel、WaitGroup、Channel、Context等核心工具的源码级分析读完本文你将掌握 Hyperf 协程编程的全部关键技能。协程概念为什么 Hyperf 需要协程PHP-FPM 的阻塞运行模式在介绍协程之前先回顾传统PHP-FPM架构的运行方式。PHP-FPM是一个多进程的FastCGI管理器绝大多数 PHP 应用都使用它。假设用Nginx提供 HTTP 服务使用Apache同理所有客户端请求先到达NginxNginx通过FastCGI协议把请求转发给PHP-FPM处理PHP-FPM的Master Process会为每个请求分配一个Worker Process。这种处理模式意味着整个进程在解析 PHP 脚本与等待业务结果之间是阻塞等待的处理完后回收子进程。也就是说你有多少个PHP-FPM进程就能同时处理多少个请求。假设PHP-FPM有200个Worker Process每个请求耗时1秒那么整个服务器理论上最大QPS只有200/s。在高并发场景下这样的性能往往不够。虽然可以用Nginx做负载均衡、多台PHP-FPM服务器提供服务但由于PHP-FPM阻塞等待的模型一个请求至少会占用一个MySQL连接多节点会产生大量MySQL连接而MySQL默认最大连接数是100虽然可以修改但这种模式显然无法很好地应对高并发场景。异步非阻塞系统及其代价在高并发场景下异步非阻塞有明显优势Worker Process不再同步阻塞地处理一个请求而是可以同时处理多个请求在I/O等待期间不阻塞并发能力极强能同时发起或维持大量请求。但异步非阻塞最直观的缺点就是回调地狱业务逻辑必须写在对应的回调函数里如果业务逻辑有多个I/O请求就会出现多层回调嵌套。下面是Swoole 1.x时代伪代码示例$db new swoole_mysql(); $config array( host 127.0.0.1, port 3306, user test, password test, database test, ); $db-connect($config, function ($db, $r) { // Query a row of data from users table $sql select * from users where id 1; $db-query($sql, function(swoole_mysql $db, $r) { if ($r true) { $rows $db-affected_rows; // Modify a row of data after the query is successful $updateSql update users set name\new name\ where id 1; $db-query($updateSql, function (swoole_mysql $db, $r) { if ($r true) { return $this-response-end(Update Successfully); } }); } $db-close(); }); });从上面的代码可以看出几乎每个操作都需要一个回调函数复杂业务场景下回调的层层嵌套和代码结构会让人崩溃。这跟JavaScript写异步方法类似而JavaScript提供了Promise、yield generator、Async/Await等方案Promise是对回调的包装yield generator和Async/Await需要在代码中显式添加语法标签它们都是回调的替代方案但你仍需要花时间去理解其实现和语法。Swoole协程也是异步回调的一种解决方案。在 PHP 中Swoole协程和yield generator都是能让异步代码以近似同步方式编写的协程方案。明显的区别是在yield generator协程机制中每次I/O操作前都需要加yield语法才能实现协程切换每一层调用都要加yield否则会出现意外错误。而Swoole协程方案则优雅得多I/O在底层被隐式切换代码中不需要添加任何额外语法或yield协程切换是静默的大大减轻了维护异步系统的心智负担。什么是协程协程的定义是协程是由用户代码调度和管理、而非操作系统内核调度的轻量级线程即运行在用户态。可以把它直接理解为一种非标准的线程实现但切换由用户决定而不是由操作系统分配CPU时间。具体来说Swoole的每个Worker Process都有一个协程调度器Scheduler来调度协程协程切换的时机是发生I/O操作或显式代码切换时。进程以单线程方式运行协程这意味着一个进程内同一时刻只有一个协程在运行且切换时机明确所以不需要像多线程编程那样处理同步锁问题。单个协程内部的代码仍然是串行执行的。在 HTTP 协程服务器中可以理解为每个请求就是一个协程。例如假设request A创建了coroutine Arequest B创建了coroutine B。处理coroutine A时代码运行到查询MySQL处此时coroutine A触发协程切换coroutine A继续等待I/O设备返回结果调度器切换到coroutine B开始处理它的逻辑遇到另一个I/O操作时再次触发切换回到coroutine A从刚才切出的地方继续执行以此类推。遇到I/O操作就切换到其他协程继续执行而不是阻塞等待。这里有一个关键问题coroutine A的MySQL查询操作必须是异步非阻塞的否则协程调度器无法切换到另一个协程继续执行因为阻塞。这是协程编程中需要避免的问题之一。协程与普通线程的区别协程是轻量级线程协程和线程都适用于多任务场景。从这个角度看协程与线程非常相似都有各自的上下文可以共享全局变量。但区别在于并发粒度不同多个线程可以同时运行但Swoole协程同一时刻只能运行一个其他协程会被暂停调度方式不同普通线程是抢占式的哪个线程能拿到资源由操作系统决定协程是协作式的执行权由用户态分配。协程编程注意事项协程内不能存在阻塞代码协程中的阻塞代码会导致协程调度器无法切换到其他协程继续执行代码因此必须防止协程中出现阻塞代码。假设我们启动了4个Worker处理 HTTP 请求通常Worker数量与CPU核心数相同或为核心数的2倍如果协程中存在阻塞代码理论上每个请求阻塞1秒应用QPS就会退化成4/s无疑退化成了与PHP-FPM类似的情况所以协程中绝不允许存在阻塞代码。那么哪些是阻塞代码可以简单认为非 Swoole 提供的大多数异步函数都是阻塞代码包括MySQL、Redis、Memcache、MongoDB、HTTP、Socket、文件操作、sleep/usleep等这些几乎覆盖了日常操作的全部。如何解决Swoole提供了 MySQL、PostgreSQL、Redis、HTTP、Socket的协程客户端。此外Swoole 4.1之后提供\Swoole\Runtime::enableCoroutine()函数可以把大部分阻塞代码协程化——只需在创建协程前执行\Swoole\Runtime::enableCoroutine()Swoole会把所有使用php_stream的 socket 转为协程调度可以理解为最常见的操作都被协程化除了curl。在Hyperf中框架已经为你处理好了这一切你只需要注意\Swoole\Runtime::enableCoroutine()仍然无法自动协程化的阻塞代码。另外从源码可以看到src/coroutine/src/Functions.php 中的run()函数会在非协程环境下调用Runtime::enableCoroutine($flags)开启 Hook默认SWOOLE_HOOK_ALL执行完回调后调用Runtime::enableCoroutine(0)关闭并明确要求只能在非协程环境中使用协程环境会抛出RuntimeException。不能用全局变量存储状态在Swoole常驻内存应用下Worker内的全局变量是在Worker内共享的而从协程介绍可知同一个Worker内会存在多个协程。协程切换意味着一个Worker会在一个时间段内处理多个协程可直接理解为多个请求如果使用全局变量存储状态状态数据可能被多个协程使用导致不同请求或不同协程之间数据混乱。这里的全局变量指$_GET/$_POST/$_REQUEST/$_SESSION/$_COOKIE/$_SERVER等$_开头的变量、global变量以及static属性或变量。那么需要这些功能时应该怎么办全局变量全局变量由Request生成Hyperf 的Request/Response由 hyperf/http-message 通过实现PSR-7标准构建所有全局变量都可以在Request对象中找到global变量与static变量在PHP-FPM模式下它们的本质是存活于一个请求生命周期内而在Hyperf中因为CLI应用特性存在两种长期生命周期全局周期Global cycle只需要创建一个静态变量供全局调用。静态变量意味着服务启动后任何协程和代码逻辑都共享该静态变量中的数据即存储的数据不能特指某个请求或某个协程协程周期Coroutine cycle由于Hyperf会自动为每个请求创建协程处理协程周期在这里也可理解为请求周期。在协程中所有状态数据都应存储在Hyperf\Context\Context类中通过该类的get、set方法读写任意结构的数据。在Context协程上下文中get或set任何数据都被限制在调用该函数的对应协程内协程结束时相关上下文数据也会自动销毁。协程最大数量通过Swoole Server的set方法设置max_coroutine参数可以配置一个Worker进程内最多可存在的协程数量。由于Worker进程处理的协程数量增加相应的内存占用也会增加为避免超过PHP的memory_limit限制应根据实际业务压测结果设置该值。Swoole的默认值是3000而hyperf-skeleton项目默认设置为100000。协程的使用创建协程使用Hyperf\Coroutine\co(callable $callable)或Hyperf\Coroutine\go(callable $callable)函数或Hyperf\Coroutine\Coroutine::create(callable $callable)方法即可简单创建一个协程协程内可以使用协程相关方法和客户端。从源码看src/coroutine/src/Functions.php 中的co()与go()都是对Coroutine::create()的封装成功时返回协程 ID失败返回false。而 src/coroutine/src/Coroutine.php 中create()内部通过Hyperf\Engine\Coroutine::create()创建协程并用try/catch包裹回调以捕获异常并记录日志通过FormatterInterface格式化后交给StdoutLoggerInterface输出创建的协程 ID 可通过getId()获取异常时返回-1。这意味着即使在协程内抛出异常也不会导致整个进程崩溃。判断是否运行在协程环境中某些场景下我们希望判断当前是否运行在协程环境中作为兼容协程环境和非协程环境代码的判断依据可以使用Hyperf\Coroutine\Coroutine::inCoroutine(): bool方法获取结果。其实现是Co::id() 0即当前协程 ID 大于0即认为处于协程环境。获取协程 ID某些场景下需要根据协程 ID 做逻辑处理比如协程上下文可以通过Hyperf\Coroutine\Coroutine::id(): int获取当前协程 ID如果不在协程环境中该方法返回-1。此外src/coroutine/src/Coroutine.php 还提供了几个相关方法parentId(?int $coroutineId null): int返回父协程 ID在顶层协程中返回0在非协程环境会抛出RunningInNonCoroutineException协程已销毁则抛出CoroutineDestroyedExceptionsleep(float $seconds): void协程内睡眠指定秒数支持浮点数fork(callable $callable, array $keys [])创建携带父协程上下文副本的新协程内部调用Context::copy($cid, $keys)复制上下文stats()、exists(int $id)、list()分别获取协程统计信息、判断协程是否存在、遍历当前协程列表。Channel与 Go 语言的chan类似Channel支持多生产者协程与多消费者协程模式底层自动实现协程的切换和调度。Channel与 PHP 的数组类似只占用内存没有其他额外资源申请所有操作都是内存操作、没有I/O用法类似于SplQueue队列。Channel主要用于协程间通信。当想把数据从一个协程返回给另一个协程时可以通过Channel传递。主要方法Channel-push当队列中有其他协程在等待pop数据时会按顺序自动唤醒一个消费者协程当队列满时自动yield让出控制权等待其他协程消费数据Channel-pop当队列为空时自动yield等待其他协程产生数据数据被消费后队列可以推入新数据并自动按顺序唤醒一个生产者协程。下面是一个简单的协程间通信示例?php co(function () { $channel new \Swoole\Coroutine\Channel(); co(function () use ($channel) { $channel-push(data); }); $data $channel-pop(); });需要注意的是在hyperf/coroutine组件内部底层实际上使用的是Hyperf\Engine\Channel例如WaitGroup、Parallel、Concurrent等类均基于它实现这层抽象让组件可以兼容 Swoole 与 Swow 两种引擎。Defer当想在协程结束时运行一些代码可以使用defer(callable $callable)函数或Hyperf\Coroutine::defer(callable $callable)把一个函数以栈的形式存放。存放后栈中的函数会在当前协程结束时逐个执行执行顺序为 LIFO后进先出Last in, First out。从源码看src/coroutine/src/Coroutine.php 中defer()直接调用Hyperf\Engine\Coroutine::defer()并且同样用try/catch包裹回调保证 defer 回调中的异常只会被记录而不会中断协程销毁流程。典型场景是释放资源、关闭连接、记录日志等收尾操作。WaitGroupWaitGroup是一个源自Channel的功能特性。如果你了解Go语言会知道其中的WaitGroup。在Hyperf中WaitGroup的目的是阻塞主协程等待所有相关子协程完成任务后再继续运行。这里说的阻塞等待只针对主协程即当前协程不会阻塞当前进程。?php $wg new \Hyperf\Coroutine\WaitGroup(); // Counter increase 2 $wg-add(2); // Create coroutine A co(function () use ($wg) { // some code // Counter decrease 1 $wg-done(); }); // Create coroutine B co(function () use ($wg) { // some code // Counter decrease 1 $wg-done(); }); // Wait for coroutine A and coroutine B finished $wg-wait();注意WaitGroup本身也需要在协程中使用。从源码看src/coroutine/src/WaitGroup.php 的实现与 Go 语义一致内部基于容量为1的Channel通过计数器count管理任务数。add()在waiting状态下调用会抛出BadMethodCallException计数器为负会抛出InvalidArgumentExceptiondone()使计数器减一减到0且正在等待时向 Channel 推送信号唤醒等待者wait(float $timeout -1)支持超时参数默认-1表示无限等待且不允许重复等待。ParallelParallel是 Hyperf 基于WaitGroup特性提供的抽象比WaitGroup使用更方便。代码如下?php $parallel new \Hyperf\Coroutine\Parallel(); $parallel-add(function () { \Hyperf\Coroutine\Coroutine::sleep(1); return \Hyperf\Coroutine\Coroutine::id(); }); $parallel-add(function () { \Hyperf\Coroutine\Coroutine::sleep(1); return \Hyperf\Coroutine\Coroutine::id(); }); // $result is [1, 2] $result $parallel-wait();从上面的代码可以看出只用了1秒就获取到了两个不同协程的 ID。调用add(callable $callable)时Parallel类会自动为其创建协程并加入WaitGroup的调度器。不仅如此还可以使用parallel(array $callables)函数进一步简化上述代码达到同样的效果?php use Hyperf\Coroutine\Coroutine; // The passed array parameters can also use key of array to facilitate distinguish the result of coroutine, and the returned result will also return the corresponding result according to key. $result parallel([ function () { Coroutine::sleep(1); return Coroutine::id(); }, function () { Coroutine::sleep(1); return Coroutine::id(); } ]);注意Parallel本身也需要在协程中使用。Parallel 的并发限制与异常处理源码级从 src/coroutine/src/Parallel.php 可以看到一些文档未展开、但非常实用的细节并发限制构造函数支持int $concurrent参数0表示不限制并发数传入大于0的值时内部创建容量为该值的Channel作为信号量在wait()中每个回调执行前先push占位、结束后pop释放从而限制同时运行的协程数量结果按 key 返回add(callable $callable, $key null)支持传入 keywait()返回的数组会按 key 对应结果测试用例 src/coroutine/tests/ParallelTest.php 中验证了字符串 key、整数 key、浮点 key 以及无 key 下标场景异常聚合wait()默认$throw true任何子协程抛出异常都会收集到throwables中最终抛出ParallelExecutionException该异常通过getResults()与getThrowables()暴露已成功的部分结果和全部异常详情见 src/coroutine/src/Exception/ParallelExecutionException.php测试testParallelThrows、testParallelResultsAndThrows对此有完整验证可复用count()查看已添加回调数量clear()清空回调与结果实例可多次调用wait()。此外parallel()函数还支持第二个参数int $concurrent 0限制并发数见 src/coroutine/src/Functions.phpParallelTest::testParallelConcurrent验证了并发限制下结果的行为。Coroutine Context协程上下文由于同一进程中的协程共享内存协程的执行/切换是非顺序的这意味着很难控制当前是哪一个协程在运行事实上可以控制但没人愿意这样做所以需要能够在协程切换时同步切换对应的上下文。Hyperf 实现协程上下文管理非常简单基于Hyperf\Context\Context的静态方法set(string $id, $value)、get(string $id, $default null)、has(string $id)即可完成上下文数据的管理。这些方法设置和获取的值都限定在当前协程内协程结束时对应上下文会自动释放无需手动管理也无需担心内存泄漏的风险。Context 实现细节从 src/context/src/Context.php 源码可以看到其实现逻辑在协程环境中Coroutine::id() 0数据存储在Coroutine::getContextFor($coroutineId)返回的协程上下文中在非协程环境中则存储于静态数组$nonCoContext因此非协程环境下的代码也可以安全使用Context除了文档提到的三个方法还提供了destroy(string $id)显式销毁某个上下文键、copy(int $fromCoroutineId, array $keys [])从其他协程复制上下文配合Coroutine::fork()使用、override(string $id, Closure $closure)用闭包覆盖值、getOrSet(string $id, $value)不存在时再设置这些方法让上下文管理更加灵活上下文值存放在ArrayObject容器中协程结束时自动回收这正是文档所述无需担心内存泄漏的底层保证。实用示例协程上下文与 defer 结合下面是一个综合示例展示如何在子协程中安全地存取上下文并在协程结束时清理?php use Hyperf\Context\Context; use function Hyperf\Coroutine\co; use function Hyperf\Coroutine\defer; co(function () { // 将数据绑定到当前协程上下文 Context::set(user_id, 1001); // 协程结束时自动清理 defer(function () { Context::destroy(user_id); }); // 在另一个协程中读取不会拿到当前协程的数据 co(function () { var_dump(Context::get(user_id)); // null }); var_dump(Context::get(user_id)); // 1001 });更多协程辅助工具除了文档重点讲解的工具外hyperf/coroutine 组件还提供了若干实用的协程辅助类可从 src/coroutine/src 目录查看Waitersrc/coroutine/src/Waiter.php将闭包放入子协程执行并通过 Channel 等待结果默认超时10秒超时抛出WaitTimeoutException子协程异常会以ExceptionThrower包装后重新抛出。wait(Closure $closure, ?float $timeout null)函数见 src/coroutine/src/Functions.php是它的快捷调用方式Concurrentsrc/coroutine/src/Concurrent.php用容量为$limit的 Channel 限制并发协程数量create()提交任务超出限制时自动等待并支持length()、isFull()、isEmpty()等状态查询Barriersrc/coroutine/src/Barrier.php基于Hyperf\Engine\Barrier的同步屏障用于在指定数量的协程到达后统一放行Locker、Mutex协程内的锁原语用于在协程间保护临界资源避免协程切换带来的竞态问题Channel管理器src/coroutine/src/Channel 目录下的Caller、Manager、Pool提供 Channel 的池化与管理能力。这些工具与文档所述的核心能力共同构成了 Hyperf 协程编程的完整工具箱相关的单元测试可参考 src/coroutine/tests 目录。总结协程是 Hyperf 高性能的基石。理解PHP-FPM阻塞模型与协程非阻塞调度的本质区别遵守协程内不放阻塞代码、不用全局变量存状态两条铁律熟练使用co/go创建协程、Channel通信、WaitGroup/Parallel并行、Context上下文管理以及defer收尾是写出正确、高效 Hyperf 应用的必备技能。本文结合 docs/en/coroutine.md 与 src/coroutine 源码测试见 src/coroutine/tests对每个工具的实现进行了印证建议读者在实际业务中优先使用Parallel、Waiter等高层抽象并把上下文状态统一收敛到Hyperf\Context\Context中即可在享受协程高性能的同时规避常见的并发陷阱。赞分享后端微服务【免费下载链接】hyperf A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.项目地址https://gitcode.com/gh_mirrors/hy/hyperf点击查看免费下载相关推荐pi Coding Agent 提供商接入指南订阅登录、API Key 与云厂商凭证解析实战pi Coding Agent 提供商接入指南订阅登录、API Key 与云厂商凭证解析实战 本文基于 piAI agent toolkit仓库中 pro人工智能大模型AI Agent代码智能体AI 应用工具调用Hyperf 协程编程实战指南从 PHP-FPM 阻塞模型到高性能异步架构Hyperf 协程编程实战指南从 PHP FPM 阻塞模型到高性能异步架构 Hyperf 是基于 Swoole 协程构建的高性能常驻内存框架协程是其高性能特后端Web框架微服务RPC框架异步编程Hyperf协程编程实战解锁PHP异步编程的无限潜力 Hyperf协程编程实战解锁PHP异步编程的无限潜力 Hyperf是一个基于Swoole协程的高性能PHP框架它彻底改变了传统PHP的同步阻塞编程模式后端微服务上一篇突破性能瓶颈llama.cpp分布式KV缓存技术深度解析下一篇突破算力瓶颈llama.cpp分布式推理实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表