zoukankan      html  css  js  c++  java
  • 测试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;
             }
    }
    ?>      

    结果:

  • 相关阅读:
    HUST 1372 marshmallow
    HUST 1371 Emergency relief
    CodeForces 629D Babaei and Birthday Cake
    CodeForces 629C Famil Door and Brackets
    ZOJ 3872 Beauty of Array
    ZOJ 3870 Team Formation
    HDU 5631 Rikka with Graph
    HDU 5630 Rikka with Chess
    CodeForces 626D Jerry's Protest
    【POJ 1964】 City Game
  • 原文地址:https://www.cnblogs.com/LO-gin/p/14379556.html
Copyright © 2011-2022 走看看