当前位置: 首页 > article >正文

php使用yield处理大数据文件

1.概述

yield和return有点像,它是一个生成器,对PHP应用的性能有非常大的影响, 只有在调用的时候才会执行,并不产生多余的值,比如读取一个很大的文件或者计算大量的数据时,如果直接读取很有可能内存就爆了,而yield就可以做到把数据一行行读取到php运行内存,并非一次性读取到php运行内存,这样在运行时就会节省大量的内存;还有就像在循环数据库的所有记录的时候,这个记录可能会很大,例如100万行,那么通过传统的办法就行不通了,首先通过web访问,php有超时时间,默认是30秒,那么就只能通过php cli的方式来执行,这样就没有超时时间了,但由于数据量太大,php循环时需要将数据全部载入内存中执行,由于数据量太大,php会报Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 4096 bytes)的错误,php默认的内存限制为128M,虽然可以通过修改php.ini中memory_limit的选项的值来提高内存限制,但始终不是最优的解决办法,这时通过生成器函数就可以极大节省内存开销

2.案例引入

先看一个简单的函数

<?php
function createRange($number){
  $data = [];
  for($i=0;$i<$number;$i++){
      $data[] = time();
  }
  return $data;
}
?>

这是一个非常常见的PHP函数,我们在处理一些数组的时候经常会使用。这里的代码也非常简单:

创建一个函数, 函数内包含一个for循环,循环的把当前时间放到data里面for循环执行完毕,把data返回出去。

再写一个函数,把这个函数的返回值循环打印出来:

<?php
$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
  sleep(1);//这里停顿1秒,后续有用
  echo $value.'<br />';
}

在浏览器里面看一下运行结果,非常赖斯,没有任何问题,这时需要思考一个问题:

        在调用函数createRange的时候给$number的传值是10,一个很小的数字,如果现在传递一个值10000000(1000万),那么,在函数createRange里面,for循环就需要执行1000万次,且有1000万个值被放到$data里面,而$data数组在是被放在内存里的,所以,在调用函数时候会占用大量内存,非常的不友好,这时生成器(yield)就闪亮登场了,修改一下代码,创建生成器

<?php
function createRange($number){
  for($i=0;$i <= $number;$i++){
      yield time();
  }
}
?>

这段代码,删除了数组$data,也没有返回任何内容,而是在time()之前使用了一个关键字yield

使用生成器

<?php
$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
  sleep(1);
  echo $value.'<br />';
}
?>

运行上面的代码,可以发现:

        输出的值和第一次没有使用生成器的不一样,这里的值(时间戳)中间间隔了1秒,这里的间隔一秒其实就是sleep(1)造成的,但是为什么第一次没有间隔呢,那是因为:未使用生成器时,createRange函数内的for循环结果被很快放到$data中,并且立即返回,所以,foreach循环的是一个固定的数组; 使用生成器时,createRange的值不是一次性快速生成,而是依赖于foreach循环,foreach循环一次,for执行一次

3.上述代码的执行过程

<?php
function createRange($number){
  for($i=0;$i <= $number;$i++){
      yield time();
  }
}

$result = createRange(10); // 这里调用上面创建的函数
foreach($result as $value){
  sleep(1);
  echo $value.'<br />';
}
?>

(1).首先调用createRange函数,传入参数10,但是for值执行了一次然后停止了,并且告诉foreach第一次循环可以用的值

(2).foreach开始对$result循环,进来首先sleep(1),然后开始使用for给的一个值执行输出

(3).foreach准备第二次循环,开始第二次循环之前,它向for循环又请求了一次

(4).for循环于是又执行了一次,将生成的时间戳告诉foreach

(5).foreach拿到第二个值,并且输出。由于foreach中sleep(1),所以,for循环延迟了1秒生成当前时间,所以,整个代码执行中,始终只有一个记录值参与循环,内存中也只有一条信息

结果:

        所以无论开始传入的$number有多大,由于并不会立即生成所有结果集,所以内存始终是一条循环的值

4.生成器的原理:

生成器yield关键字不是返回值,他的专业术语叫产出值,只是生成一个值,代码中foreach循环其实是PHP在使用生成器的时候,会返回一个Generator类的对象,foreach可以对该对象进行迭代,每一次迭代,PHP会通过Generator实例计算出下一次需要迭代的值,这样foreach就知道下一次需要迭代的值了,而且,在运行中for循环执行后,会立即停止。等待foreach下次循环时候再次和for索要下次的值的时候,for循环才会再执行一次,然后立即再次停止,直到不满足条件不执行结束

5.案例演示

5.1 读取text文件  我们创建一个text文本文档,并在其中输入几行文字,示范读取

<?php
header("content-type:text/html;charset=utf-8");
function readTxt()

{
  # code...
  $handle = fopen("./test.txt", 'rb');

  while (feof($handle)===false) {
      # code...
      yield fgets($handle);
  }

  fclose($handle);
}


foreach (readTxt() as $key => $value) {
  # code...
  echo $value.'<br/>';
}

通过上图的输出结果我们可以看出代码完全正常,但是,背后的代码执行规则却一点儿也不一样,使用生成器读取文件,第一次读取了第一行,第二次读取了第二行,以此类推,每次被加载到内存中的文字只有一行,大大的减小了内存的使用。这样,即使读取上G的文本也不用担心,完全可以像读取很小文件一样编写代码

5.2 读取数据库用户数据,处理相关逻辑

参考:redis实现统计活跃用户  功能

<?php

class UserController {

    public function actionGetUserIds()
    {
        //查询到的用户id组成一个数组(该数组可能会很大)
        $userIds = $this->get_bitmap_all($key);
        if ($userIds) {
            //循环查询(使用yield生成器)用户表,处理用户逻辑相关功能
            $i_userIds = $this->YieldUser($userIds);
            foreach ($i_userIds as $userId) {
                 //获取通过生成器处理的用户id,处理用户相关功能
                 ...
            }
        }
        
    }
    
   /**
     * 获取bitmap
     * @date 2021/11/12
     * @param $key
     * @return array
     */
    public function get_bitmap_all($key)
    {
        //服务端redis
        $redis = Yii::$app->redis;

        $result = [];
        $value = $redis->get($key);
        if ($value) {
            /**
             * 解包(redis返回来的是二进制字符串,我们需要把它解成对应的数字)
             */
            $bitmap = unpack('C*', $value);
            if ($bitmap) {
                foreach ($bitmap as $key => $number) {
                    // 下标是从1开始的; 1个字节8位
                    $offset = ($key - 1) * 8;
                    // 过滤没有标记的字节段
                    if ($number) {
                        for ($i = 0; $i < 8; $i++) {
                            // 遍历这个字节的每一位,是否有为1的值,如果有,那就记录这个位置的偏移量,就是用户id
                            if (($number >> $i & 1) == 1) {
                                // 8位范围是0~7,因为redis是高位到低位存储,所以要反过来计算偏移量
                                $result[] = $offset + (7 - $i);
                            }
                        }
                    }
                }
            }
        }
        return $result;
    }


    /**
     * yield生成器
     * @param $userIds
     * @return \Generator
     */
    public function YieldUser($userIds) {
        //使用yield生成器
        foreach ($userIds as $userId) {
            yield $userId;
        }
    }
}

到这里,php使用yield处理大数据文件基本功能就完成了


http://www.kler.cn/a/4355.html

相关文章:

  • 非安全函数
  • 【Compose multiplatform教程】05 IOS环境编译
  • 【论文阅读】具身人工智能(Embodied AI)综述:连接数字与物理世界的桥梁
  • 数据结构--二叉树
  • 关于在 Kotlin DSL 中,ndk 的配置方式
  • 【 PID 算法 】PID 算法基础
  • Python中 join() 函数的使用方法
  • vue+springboot贫困山区儿童衣物捐赠网站系统
  • 基于Spring、Spring MVC、MyBatis的共享单车管理系统
  • Studio One没有声音怎么办 Studio One工程没有声音
  • Go底层原理:一起来唠唠GMP调度(一)
  • 【华为OD机试 2023最新 】 区块链文件转储系统(C++ 100%)
  • IO的类型(BIO、NIO、AIO)
  • GameFramework框架详解之 Config全局配置
  • Nacos配置中心优雅配置JSON数据格式
  • 第二类斯特林数
  • 多少个X 蓝桥杯模拟
  • dp-一和零
  • 『Linux从入门到精通』第 ⑧ 期 - 项目自动化构建工具——make/Makefile
  • 解决华为云鲲鹏arm架构运行IoTDB经常自动挂掉
  • 腾讯云短信接入发送短信 Java
  • CXL 2.0白皮书解读翻译:计算快速链路,相对于CXL 1.1的改进,有哪些提升和要求
  • 【索引失效如何排查】
  • 100种思维模型之反脆弱思维模型-40
  • 最全总结---36种MySQL时间函数
  • 芯片工程师的ChatGPT初体验(下)