hyperf协程

hyperf
了解协程之前先需要知道php-fpm的运作模式

php-fpm的运作模式

PHP-FPM 是一个多进程的 FastCGI 管理程序,是绝大多数 PHP 应用所使用的运行模式。假设我们使用 Nginx 提供 HTTP 服务(Apache 同理),所有客户端发起的请求最先抵达的都是 Nginx,然后 Nginx 通过 FastCGI 协议将请求转发给 PHP-FPM 处理,PHP-FPM 的 Worker 进程 会抢占式的获得 CGI 请求进行处理,这个处理指的就是,等待 PHP 脚本的解析,等待业务处理的结果返回,完成后回收子进程,这整个的过程是阻塞等待的,也就意味着 PHP-FPM 的进程数有多少能处理的请求也就是多少,假设 PHP-FPM 有 200 个 Worker 进程,一个请求将耗费 1 秒的时间,那么简单的来说整个服务器理论上最多可以处理的请求也就是 200 个,QPS 即为 200/s,在高并发的场景下,这样的性能往往是不够的,尽管可以利用 Nginx 作为负载均衡配合多台 PHP-FPM 服务器来提供服务,但由于 PHP-FPM 的阻塞等待的工作模型,一个请求会占用至少一个 MySQL 连接,多节点高并发下会产生大量的 MySQL 连接,而 MySQL 的最大连接数默认值为 100,尽管可以修改,但显而易见该模式没法很好的应对高并发的场景。

协程

协程可以很好的解决异步非阻塞系统的开发问题
协程是一种轻量级的线程,由用户代码来调度和管理,而不是由操作系统内核来进行调度,也就是在用户态进行。可以直接的理解为就是一个非标准的线程实现,但什么时候切换由用户自己来实现,而不是由操作系统分配 CPU 时间决定。
单个协程内的代码运行仍是串行的,放在一个 HTTP 协程服务上来理解就是每一个请求是一个协程

协程与普通线程有哪些区别

  • 协程是一个轻量级的线程,协程和线程都适用于多任务的场景下,都有自己的上下文,可以共享全局变量
  • 但在同一时间可以有多个线程处于运行状态,对于 Swoole 协程来说只能有一个,其它的协程都会处于暂停的状态
  • 普通线程是抢占式的,哪个线程能得到资源由操作系统决定,而协程是协作式的,执行权由用户态自行分配

创建协程

快捷函数

  • Hyperf\Coroutine\co(callable $callable)
  • Hyperf\Coroutine\go(callable $callable)
  • Hyperf\Coroutine\Coroutine::create(callable $callable)
    即可创建一个协程,协程内可以使用协程相关的方法和客户端

Coroutine类

Hyperf\Coroutine\Coroutine::create(callable $callable)

Parallel 工具

$parallel = new Parallel();
$parallel->add(function () {
    sleep(1);
    return Coroutine::id();
});
$parallel->add(function () {
    sleep(1);
    return Coroutine::id();
});

try{
    // $results 结果为 [1, 2]
   $results = $parallel->wait(); 
} catch(ParallelExecutionException $e){
    // $e->getResults() 获取协程中的返回值。
    // $e->getThrowables() 获取协程中出现的异常。
}

parallel(array $callables) 函数进行更进一步的简化上面的代码

// 传递的数组参数您也可以带上 key 便于区分子协程,返回的结果也会根据 key 返回对应的结果
$result = parallel([
    function () {
        sleep(1);
        return Coroutine::id();
    },
    function () {
        sleep(1);
        return Coroutine::id();
    }
]);

Hyperf 协程的核心特性与工具

协程上下文管理 (Hyperf\Context\Context)

用于替代全局变量和静态属性,安全地存储请求级别或协程级别的独有数据。

// 设置值
use Hyperf\Context\Context;
Context::set('request_id', uniqid());
// 获取值
$requestId = Context::get('request_id');
// 判断是否存在
$hasValue = Context::has('request_id');

并发控制与同步工具

  • WaitGroup:用于等待一组协程全部执行完毕。
  • Channel:用于协程间的通信,类似于 Go 语言的 chan。
  • Concurrent:用于控制同一时间运行的协程数量(并发度),常用于限流场景。

Defer 特性

用于在当前协程结束时自动执行一些清理任务(如关闭文件句柄、归还数据库连接到连接池),能有效预防资源泄漏

注意事项

  • 绝对禁止使用全局变量储存状态$_GET、$_POST、global、类的静态属性(static)在常驻内存的 Worker 进程中会被所有协程(即所有请求)共享,必然导致数据错乱。所有请求级数据都应通过 Context 存取。

  • 避免阻塞操作:协程内必须使用非阻塞的客户端或操作,否则会拖垮整个 Worker 进程的并发能力。务必使用 Hyperf 提供的协程客户端(如数据库连接池、协程版 Redis、Guzzle HTTP 客户端)
    eg:file_get_contents 就是一个阻塞I/O,正确:使用协程化文件操作或异步任务队列

  • 方法1. 使用 Hyperf 提供的协程化文件读取函数
    System::readFile 是协程友好的,遇到 I/O 等待时会让出控制权
    $result = System::readFile(‘/path/to/large/data.json’)

  • 方法2. 将耗时文件操作投递到单独的 TaskWorker 进程池
    将耗时文件操作投递到单独的 TaskWorker 进程池
    $result = $this->taskDriver->push(new FileReadTask(‘/path/to/file‘));

  • 注意父子协程的同步:在父协程中创建子协程后,如果父协程没有等待(如使用 WaitGroup 或 Parallel),父协程可能先于子协程结束,导致子协程无法从父协程的上下文中获取正确数据

  1. WaitGroup 的用途是使得主协程一直阻塞等待直到所有相关的子协程都已经完成了任务后再继续运行,这里说到的阻塞等待是仅对于主协程(即当前协程)来说的,并不会阻塞当前进程。
<?php
$wg = new \Hyperf\Coroutine\WaitGroup();
// 计数器加二
$wg->add(2);
// 创建协程 A
co(function () use ($wg) {
    // some code
    // 计数器减一
    $wg->done();
});
// 创建协程 B
co(function () use ($wg) {
    // some code
    // 计数器减一
    $wg->done();
});
// 等待协程 A 和协程 B 运行完成
$wg->wait();

  • 依赖注入容器的单例特性:通过依赖注入容器获取的类实例(如 Service 类)在进程内是单例,且被多个协程共享。因此,绝不能将任何请求相关的数据作为这类对象的属性,否则会造成数据污染。临时数据应存储在 Context 中。
    eg:通过依赖注入的方式去编写程序新增查询用户数据
    请求A填入数据“张”,同一时间,另一个请求B填入数据“李”,当请求A稍后调用其他方法时,可能读到的是用户B的数据
    此时需要使用协程上下文Context的方式存储请求级数据
<?php
declare(strict_types=1);

namespace App\Service;

use Hyperf\Context\Context;

class SafeUserService
{
    // 使用 Context,key 中加入服务前缀避免冲突
    private const CONTEXT_KEY = ‘user_service.current_user’;
    
    public function setCurrentUser(array $user): void
    {
        // 🎯 将数据存储在当前协程/请求的上下文中,与其他请求完全隔离
        Context::set(self::CONTEXT_KEY, $user);
    }
    
    public function getCurrentUser(): ?array
    {
        // 🎯 从当前协程/请求的上下文中获取数据
        return Context::get(self::CONTEXT_KEY);
    }
}
  • 谨慎使用 @Inject 和 @Value 注解:这两个注解主要用于在服务启动阶段进行依赖注入和配置读取。对于某些惰性加载(用时才创建)的资源,直接注入可能会导致问题。
    ❌ 问题示例:注入带有物理连接的客户端,可能导致启动失败
<?php
declare(strict_types=1);

namespace App\Controller;

use Hyperf\HttpServer\Annotation\AutoController;
use Psr\Container\ContainerInterface;
use App\Service\SomeRemoteService;

#[AutoController]
class SafeOrderController extends AbstractController
{
    public function __construct(
        // 🎯 注入容器,而不是具体的服务
        protected ContainerInterface $container
    ) {}
    
    public function create()
    {
        // 方法1:使用时才从容器获取(惰性加载)
        $remoteService = $this->container->get(SomeRemoteService::class);
        
        // 方法2(更优雅):为 SomeRemoteService 使用代理类或工厂模式
        // Hyperf 的懒加载代理可以完美解决此问题
        // 在 `config/autoload/annotations.php` 中启用 ‘lazy_loading’
        // 然后直接注入即可,框架会在第一次调用方法时才实例化真实对象
        // $this->remoteService->handle();
        
        return $remoteService->handle();
    }
}

✅ 正确做法:注入容器或工厂,使用时再创建连接

<?php
declare(strict_types=1);

namespace App\Controller;

use Hyperf\HttpServer\Annotation\AutoController;
use Psr\Container\ContainerInterface;
use App\Service\SomeRemoteService;

#[AutoController]
class SafeOrderController extends AbstractController
{
    public function __construct(
        // 🎯 注入容器,而不是具体的服务
        protected ContainerInterface $container
    ) {}
    
    public function create()
    {
        // 方法1:使用时才从容器获取(惰性加载)
        $remoteService = $this->container->get(SomeRemoteService::class);
        
        // 方法2(更优雅):为 SomeRemoteService 使用代理类或工厂模式
        // Hyperf 的懒加载代理可以完美解决此问题
        // 在 `config/autoload/annotations.php` 中启用 ‘lazy_loading’
        // 然后直接注入即可,框架会在第一次调用方法时才实例化真实对象
        // $this->remoteService->handle();
        
        return $remoteService->handle();
    }
}

对于 @Value 注解的注意事项:@Value 是在对象创建时从配置中心读取值。如果配置中心连接失败,对象创建会失败。对于非核心配置,考虑在方法内部使用 config() 函数按需读取,或结合默认值使用。

posted @ 2026-01-14 15:47  py卡卡  阅读(0)  评论(0)    收藏  举报