【workerman/webman】杂谈及一些协程的应用

chaz6chez

前言

workerman v5在经历了几年的开发和测试,于2025年元旦正式发布,webman开发框架及众多周边插件也紧随其后更新兼容了v5版本;作为PHP界老牌的网络容器,workerman的稳定性及易用性毋庸置疑,常驻内存的运行模式、多进程、多协议支持等高性能高效率的特性让许多PHPer接触到了之前不曾触摸过的技术方向;那么v5版本相较于之前版本给开发者带来了什么比较明显且用的上的特性呢?

  1. revolt/event-loop为基建的事件驱动库
  2. 兼容多方协程实现的协程功能

以上是变化较大且意义重大的特性,除此之外还有许多的优化内容及特性升级,请参考官方文档

“前生”

我个人一直认为所谓‘协程’其实是至少包含了上下文管理、协程调度器、协程执行器三部分的完整方案,在没有Fiber之前,原生PHP中其实已经有了无栈协程的相关实现,借助yield完成,但不论是Fiber还是yield,都不是完整的‘协程’,只是和上下文相关的一些功能而已,并没有调度及执行两部分。而非原生PHP中仅有swoole是一套完整的协程实现方案,不论是它早期底层类yield的异步还是现如今比较现代化的Coroutine,它们都包含了成熟的事件循环驱动(调度/执行)和丰富的上下文管理工具。

可能有人会问了,workerman也有事件循环驱动,为什么没有协程?先说结论,因为不好管理且生态不够统一,生态的支持度也不足够。

在没有Fiber之前,借助yield来实现协程方案需要保存特别特别多的栈上下文,你可以把它们理解为因为手动中断的入参参数,他们需要存放在比如静态变量(也就是内存)中等待下一次的唤醒,唤醒后继续从中断的地方执行,再主动中断的这个过程中,进程就可以交给其他的事务进行执行,整个进程内的所有不同事务呈现无序的交替运行状态,就像是我们人在工作的时候时不时去上个厕所、接杯咖啡,回来继续工作;Fiber其实也类似,只不过它并没有像yield一样直接返回暂停时的栈上下文,而是主动保存在一个特定的地方自行管理,这样就省去了自行使用内存管理的问题,简化了操作,在我每一次归来后无需再翻看我的记事本查看我到底写到那里了,而是直接就可以衔接。

workerman之前也有利用yield + promise包做的异步方案,但都要进行很多侵入式的改造,代价大于收益。

“今世”

workerman v5基于revolt/event-loop作为事件驱动引擎,一方面是由此引入Fiber,一方面是减少目前PHP开发中过多的事件驱动引擎的分化问题,另外还兼容了swowswoole的事件驱动引擎,是支持中国本土化的内容,另外本质上也是为了减少分化和加强协程的引入;就此,在workerman v5中就可以使用以上三种驱动的协程方案。

那么协程能干什么呢?
假设一个场景,我们需要创建一个异步导出任务,这个异步导出任务可能的实现方式就会是:

  1. 前端请求接口 -> 接口创建任务 -> 将任务投递至消息队列
  2. 消息队列消费导出 -> 修改任务状态
  3. 前端轮询查询任务状态及下载连接

而有了协程我们可以实现以下两种方案:

  1. 自轮询消费(查询数据如果使用阻塞方法还是会阻塞)

    public function test(): Response
    {
        $id = 'your_file_id';
        // 根据请求参数获取分片数量
        $count = YourData::getShardingCount($request = request()->all());
        for ($i = 0; $i < $count; $i++) {
            // 分片获取数据
            if ($data = YourData::getListBySharding($request, $i)) {
                // 导出追加
                YourExcel::append($id, $data);
            }
            // 协程随机出让1-10 ms
            Timer::sleep(rand(1, 10) / 1000);
        }
    
        return new Response(200, body: json_encode(YourExcel::getUrl($id)));
    }
  2. 自轮询查询方式

    public function test(): Response
    {
        $id = 'your_file_id';
        // 发布至消息队列
        YourMessageMQ::publish($id, $request = request()->all());
        while (1) {
            // 查询消息队列消费情况
            if (YourMessageMQ::isComplete($id)) {
                break;
            }
            // 协程随机出让1-10 ms
            Timer::sleep(rand(1, 10) / 1000);
        }
    
        return new Response(200, body: json_encode(YourMessageMQ::getReturn($id)));
    }

以上两种方式都不会特别的阻塞当前进程而实现一个长轮询接口,避免了前端短轮询的资源消耗问题;类似SSE的实现方式也会更简单一些,只需要利用Timer::sleep主动出让当前协程控制权给事件驱动引擎就可以,而且这样的好处是既保留了PHP系统层面的CPU控制权出让sleep/usleep又提供了事件驱动控制权的出让,区别于SwowSwoole对系统函数的hook改造,但缺点又是无法改变已经存在的阻塞式函数的阻塞调用逻辑。

“来世”

因为PHP的协程方案是单线程的,同一时刻只能运行一个任务,所以需要在事件循环内尽可能地non-blocking出让控制权,才可能让事件循环驱动在有限的时间内执行更多的任务;而目前PHP生态大多数的组件工具都是blocking的,协程所能覆盖的业务范围很窄,现存的很多协程组件并不能照顾大部分开发者的情绪,所以我真的希望在未来,PHP能够涌现更多的开发者来贡献协程相关的生态,而不是分裂,希望在有限的时间和空间内,看到这门历史不算太久的老编程语言能够焕发青春。

任重道远,也祝好

426 11 4
11个评论

咸鱼.php

赞啊

  • 暂无评论
s4160415

赞!

  • 暂无评论
xiaoming

学习中

  • 暂无评论
初心by

强的嘞

  • 暂无评论
Jinson

希望PHP能够涌现更多的开发者来贡献协程相关的生态

  • 暂无评论
极胜100

大佬的文章质量太高了。

  • 暂无评论
saithink

强强强

  • 暂无评论
hunma

  • 暂无评论
tanhongbin

大佬666

  • 暂无评论
生椰拿铁0糖

学习了

  • 暂无评论
Tinywan

学习了

  • 暂无评论

chaz6chez

4844
积分
0
获赞数
0
粉丝数
2018-11-16 加入
×
🔝