• 测试PHP几种方法写入文件的效率与安全性


    前置条件:

    所有测试生成的都写入一个新文件,如果是同一个文件名,那么每次执行脚本前,需要把该日志文件删掉,确保每次执行时日志文件都是重新创建的。

    每次执行都是往日志文件中使用多进程写入90000行日志。每种方式分成四种对照组测试:

    30*3000 加锁(即30个进程每个进程写入3000行,总共90000行,写入时需对日志文件上独占锁)。

    30*3000 不加锁(即30个进程每个进程写入3000行,总共90000行,写入时日志文件不上锁)。

    90*1000 加锁(即90个进程每个进程写入1000行,总共90000行,写入时需对日志文件上独占锁)。

    90*1000 不加锁(即90个进程每个进程写入1000行,总共90000行,写入时日志文件不上锁)。

    方式一:

    使用file_put_contents() 函数写入文件。为了避免内容覆盖,须使用FILE_APPEND模式写入。

    加锁:(n=3000 | n=1000)

    for($i=0;$i<n,$i++){

      $msg = "test text";

      file_put_contents($log, $msg, FILE_APPEND|LOCK_EX);

    }

    不加锁:(n=3000 | n=1000)

    for($i=0;$i<n,$i++){

      $msg = "test text";

      file_put_contents($log, $msg, FILE_APPEND);

    }

    执行情况如下表:

    序号

    进程数

    每个进程写入行数

    是否加锁

    第一次执行平均耗时(s)

    第二次执行平均耗时(s)

    第三次执行平均耗时(s)

    1-1

    30

    3000

    Y

    2.831

    2.815

    2.861

    1-2

    30

    3000

    N

    2.826

    2.855

    2.751

    1-3

    90

    1000

    Y

    2.407

    2.396

    2.278

    1-4

    90

    1000

    N

    1.779

    2.052

    2.01

     

     

    方式二:

    加锁:(n=3000 | n=1000)

    $handle = fopen($log,’a’);

    flock($handle,LOCK_EX);

    for($i=0;$i<n,$i++){

      $msg = "test text";

      fwrite($handle,$msg);

    }

    flock($handle,LOCK_UN);

    fclose($handle);

    不加锁:(n=3000 | n=1000)

    $handle = fopen($log,’a’);

    for($i=0;$i<n,$i++){

      $msg = "test text";

      fwrite($handle,$msg);

    }

    fclose($handle);

    执行情况如下表:

    序号

    进程数

    写入行数/每个进程

    是否加锁

    第一次执行平均耗时(s)

    第二次执行平均耗时(s)

    第三次执行平均耗时(s)

    2-1

    30

    3000

    Y

    0.66

    0.659

    0.658

    2-2

    30

    3000

    N

    1.272

    1.17

    1.161

    2-3

    90

    1000

    Y

    0.83

    0.855

    0.836

    2-4

    90

    1000

    N

    0.952

    1.097

    0.947

     

     

    以方式一跟方式二的表格为参照,同一种方式,上不上锁,性能相差不是很大,从效率上讲,方式二要比方式一高效。

    最根本的原因是file_put_contents()函数每次执行相当于执行了 fopen(),fwrite(),fclose()三个函数,所以单次执行耗时会比较长。

    如果把方式二做个调整,比如把fopen()和fclose都放进for循环里,那么方式二跟方式一基本没太大差别。比如下面代码:

    for($i=0;$i<n,$i++){

      $handle = fopen($log,’a’);

      //flock($handle,'LOCK_EX');

      $msg = "test text";

      fwrite($handle,$msg);

      //flock($handle,'LOCK_UN');

      fclose($handle);

    }

     当然,如果用这种写法本身就不合理,还不如直接使用file_put_contents()来的简单。

    不上锁的情况,日志写进去时无序的,各个进程之间穿插着写入一行日志。

    上锁的情况,日志相对有序,基本是一个进程写完n行后释放了独占锁才轮到另一个进程。但是进程之间也是无序的。比如第一个子进程写完,被第5个子进程抢到独占锁,那么就是第5个子进程先写,第二个只能继续等。所以,上锁的情况同一个进程写的日志才是有序的。

    <?php
     
    set_time_limit(30);
    $log = '/data/tmp/a.log';
    
    for($i = 0;$i<30;$i++){
        pcntl_signal(SIGCHLD, SIG_IGN);
        $fid = pcntl_fork();
        if($fid === 0){
            try {
            $start = microtime(true);       
            $handle = fopen($log,'a');
            flock($handle,LOCK_EX);
            for($j=0;$j<3000;$j++){ 
                $start_time = microtime(true);
                //TODO 其他业务逻辑
                //打点记录并行任务执行状况
                $fid = posix_getpid();
                $ffid = posix_getppid();
                $date = date('YmdHis');
                $end_time = microtime(true);
                $usetime = round($end_time-$start_time,2);
                $msg =  PHP_EOL."序号:{$i}:{$j}; 时间:{$date}; 当前进程ID:{$fid}; 父进程ID:{$ffid}; 任务开始:{$start_time}; 任务结束:{$end_time}; 耗时:{$usetime}";
                //file_put_contents($log,$msg,FILE_APPEND|LOCK_EX);
                fwrite($handle,$msg);
            }
            flock($handle,LOCK_UN);
            fclose($handle);
            unset($handle);
            $end = microtime(true);
            $s = round($end-$start,3);echo "进程:{$i},开始:{$start},结束:{$end},耗时:{$s}".PHP_EOL;
            }finally{
                if(function_exists("posix_kill")){
                    posix_kill(getmypid(),SIGTERM);
                }else{
                    system('kill -9 '.getmypid());
                }
            }
        }
    }
    
    echo 'over'.PHP_EOL;

    我们如果是使用 fopen($file,'a') 这种模式打开文件,或者file_put_contents($file,$log,FILE_APPEND) 打开文件去写入,那么写操作就不从文件描述符的当前位置开始,而是在文件末尾追加写入,每一行的写入都是一个独立的操作,所以基本没有上锁的必要。

    系统层面上对每个写入请求之前的位置更新操作应该具有原子性,且对每个写操作也是具有完整性保证的。不会导致两个写操作交叉执行的情况。

    那么在上锁的情况下,如果某个子进程在解除文件锁之前就挂掉了,会不会导致文件被锁死而导致其他进程一直等待呢?

    这里做个测试:开5个子进程,每个进程写入5行日志,日志编号序号(子进程编号:日志编号)总共25行日志。

    如果在第三个子进程上了独占锁,然后写入第三行日志前,让该子进程退出。具体过程如下:

    <?php
    set_time_limit(30);
    $log = '/data/tmp/a.log';
    
    for($i = 1;$i<=5;$i++){
        pcntl_signal(SIGCHLD, SIG_IGN);
        $fid = pcntl_fork();
        if($fid === 0){
            try {
            $start = microtime(true);
            $handle = fopen($log,'a');
            flock($handle,LOCK_EX);
            for($j=1;$j<=5;$j++){
              if($i==3 && $j==3){
                break;//第三个子进程在写入第三行日志时退出该子进程
              }
                $start_time = microtime(true);
                //TODO 其他业务逻辑
                //打点记录并行任务执行状况
                $fid = posix_getpid();
                $ffid = posix_getppid();
                $date = date('YmdHis');
                $end_time = microtime(true);
                $usetime = round($end_time-$start_time,2);
                $msg =  "序号:{$i}:{$j}; 时间:{$date}; 当前进程ID:{$fid}; 父进程ID:{$ffid}; 任务开始:{$start_time}; 任务结束:{$end_time}; 耗时:{$usetime}".PHP_EOL;
                fwrite($handle,$msg);
            }
            flock($handle,LOCK_UN);
            fclose($handle);
            unset($handle);
            $end = microtime(true);
            $s = round($end-$start,3);
            echo PHP_EOL.$s.',';
            //echo "进程:{$i},开始:{$start},结束:{$end},耗时:{$s}".PHP_EOL;
            }finally{
                if(function_exists("posix_kill")){
                    posix_kill(getmypid(),SIGTERM);
                }else{
                    system('kill -9 '.getmypid());
                }
            }
        }
    }
    
    echo 'over'.PHP_EOL;

    最终得到得日志总数时22行,因为第3个子进程只写了2行就退出了,执行结果如下图:

    由图可见,就算第三个子进程中途退出了,没有释放日志文件的独占锁,但是其他进程仍然正常按照独占的方式写入日志。

    原因是当子进程挂掉的时候,该子进程对日志文件的独占锁也会被自动解除。所以就算某个子进程上完独占锁,没来得及解除就退出了,也不用担心会影响到其他进程对该日志文件得使用。

    另外,使用 pcntl_fork() 创建进程时需要注意的一些点

    pcntl_fork()函数执行的时候,会创建一个子进程。该子进程会复制当前进程,也就是父进程的所有的变量数据,代码,还有状态。也就是说,在一个子进程创建之前,定义的变量,常量,函数等,在子进程内都可以使用。

    如果创建成功,并且该子进程在父进程内,则返回0,在子进程内返回自身的进程号,失败则返回-1。

    (1)当我们在 for 循环 或者 foreach 的循环里创建子进程,那么在子进程执行的结尾记得将子进程杀死,不然子进程也会进入 for 循环和 foreach 循环,从而形成递归创建子进程的情况。

    例如:

    $arr = array(1,2....n);

    foreach($arr as $k=>$v){
      pcntl_signal(SIGCHLD, SIG_IGN);
      $fid = pcntl_fork();

    }

    或者:

    for($i=1;$i<=n;$i++){

      pcntl_signal(SIGCHLD, SIG_IGN);
      $fid = pcntl_fork();

    }

    这两种情况最终产生的进程数有 2^n (2的n次方) ,这里面包含一个父进程,出去父进程,就有 2^n -1 个子进程。

    如果我们只是要 n 个子进程去处理,那么,就需要在每个子进程的最后将该子进程杀死。

    例如上面有部分例子的代码中在 try{} finally {} 中将子进程杀死,不让其进入递归。

    (2)不论时使用for循环还是foreach循环,都不会按照顺序去执行。

    比如第(1)部分的两个例子中,可能最后一个子进程先执行,最终先进入循环递归,结果第n个子进程执行了2n次。

    而第一个子进程进程如果最后执行到,就只能执行1次。当然这是在每个子进程执行完没有杀死的情况。比如:

    <?php
    $pid = $fid = posix_getpid();
    $arr = array('num1','num2','num3','num4');
    foreach($arr as $k=>$v){
            pcntl_signal(SIGCHLD, SIG_IGN);
            $fid = pcntl_fork();
            if($fid === 0){
                    $fid = posix_getpid();
                    $ffid = posix_getppid();                
                    $msg =  "循环次数{$v};主进程ID:{$pid}; 父进程ID:{$ffid}; 当前进程ID:{$fid};".PHP_EOL;
                    echo $msg;
             }
    }
    ?>      

    结果:

  • 相关阅读:
    Day015 PAT乙级 1013 数素数
    Day014 PAT乙级 1012 数字分类
    Day013 PAT乙级 1007 素数对猜想
    Day012 PAT乙级 1005 继续(3n+1)猜想
    Day011 PAT乙级 1003 我要通过
    Day010 PAT乙级 1002 写出这个数
    Day009 洛谷 P5707 上学迟到
    Day008 洛谷 P2181 对角线
    Day007 Java异常处理
    Fetch()
  • 原文地址:https://www.cnblogs.com/LO-gin/p/14379556.html
Copyright © 2020-2023  润新知