PHP多线程编程

来源:互联网 发布:二分法法编程 编辑:程序博客网 时间:2024/05/16 10:12
from: http://www.cnblogs.com/niniwzw/archive/2010/01/18/1651082.html

PHP多线程编程(一)

    虽然PHP 中,多线程用的比较的少。但是毕竟可能是会用到了。我最近就遇到这样一个问题,用户提交几百个url以后,要读出这个url 中的标题。

当然,你不希望用户等待的太久,10s 钟应该给出个答案。但是,本身,你要获取一个url 的标题,少的要 0.1s ,多的要好几秒。

显然,采用单个线程的方式是不行的。

 

    我的第一个设计方案是这样的:

   1. 用我前面提供的代码提供一个简单的服务器:  http://www.cnblogs.com/niniwzw/archive/2009/09/27/1575002.html

   这个服务器的作用是:提供一个url,然后,就读取标题。这里,可以每次读128个字节,看看有没有读到title,如果读到title了就停止读了。

   这样可以省带宽。

 

   2. 在客户端,同时打开1百个 socket ,访问这个服务器。如果提供的url数目超过100,那么就多次运行。

   这个方案,基本上能够满足要求,读比较快的网页如:google.com 100次,也只要1s 左右。但是,通过测试,发现,有一定

   的概率在打开链接的时候被阻塞。(有时候会阻塞个1s左右,然后继续往下open)可能打开了太多的链接了,会出很大的问题。

 

   当然,这是一个很差的解决方案:建立tcp 链接本身的消耗非常的大。因为可靠有序传输的要求,要维持一个数据结构,而且,系统还要开辟一定的缓存给客户端和服务器端,

   用户缓存数据。如果建立上百个链接,就可能占用很大的内存。作为一个系统的服务,应该尽量的简单,就是,我叫你做什么事情,你做好以后,结果给我就可以了。

  

    一般来说,PHP要进行多线程编程,比较常见的是:

    1. 要进行大量的网络耗时的操作

    2. 要做大量的运算,并且,系统有多个cpu,为了让用户有更快的体验,把一个任务,分成几个小任务,最后合并。

   

    所以,应该尽量不要在调用的地方有太多复杂的逻辑,把逻辑内置在服务中。

 

   我的第二个设计方案是这样的:

   同样用上面的服务器,只是,这个服务器功能变了,接收不超过100个的url,然后打开100个子线程,下载title。最后合并,返回给客户端。

具体怎么编写这个服务器,在下一个部分讲。

   这个一测试,发现效率高了很多。而且也十分的稳定。下载一百下google 大概 0.7s。基本上不会超过1s,而原来的那个方案,经常超过5s(20%的可能性)

 

   当然,如果这样的设计方案只是一个很简单的解决方案。如果有很多人使用你的服务的情况下,肯定不能这样做。

   PHP做企业级别的开发,一个比较复杂的问题,就是多线程怎么处理。还有就是往往采用数组 会引起内存急剧膨胀。一般,数组处理10万条数据已经是极限,

在小网站开发很少会用到一次读取如此大的数据量,要是遇到了,最好通过C 扩展进行解决,否则,一次会损耗 几百M 的内存,10个人用就拖死你。

 

PHP多线程编程(二)管道通信

一个线程如果是个人英雄主义,那么多线程就是集体主义。(不严格区分多进程 和 多线程的差别)

你不再是一个独行侠,而是一个指挥家。

独来独往,非常自由自在,但是,很多时候,不如众人拾柴火焰高。

这就是我对多线程的理解。多线程编程的主要问题是:通信 和 同步问题。

更多PHP 多线程编程的背景知识见:

PHP多线程编程(一)

在PHP 中,如果光用pcntl ,实现比较简单的通信问题都是很困难的。

 

下面介绍管道通信:

1. 管道可以认为是一个队列,不同的线程都可以往里面写东西,也都可以从里面读东西。写就是

在队列末尾添加,读就是在队头删除。

 

2. 管道一般有大小,默认一般是4K,也就是内容超过4K了,你就只能读,不能往里面写了。

 

3. 默认情况下,管道写入以后,就会被阻止,直到读取他的程序读取把数据读完。而读取线程也会被阻止,

   直到有进程向管道写入数据。当然,你可以改变这样的默认属性,用stream_set_block  函数,设置成非阻断模式。

 

下面是我分装的一个管道的类(这个类命名有问题,没有统一,没有时间改成统一的了,我一般先写测试代码,最后分装,所以命名上可能不统一):

  1. <?php
  2. class Pipe
  3. {
  4.     public $fifoPath;
  5.     private $w_pipe;
  6.     private $r_pipe;

  7.     /**
  8.      * 自动创建一个管道
  9.      *
  10.      * @param string $name 管道名字
  11.      * @param int $mode 管道的权限,默认任何用户组可以读写
  12.      */
  13.     function __construct($name= 'pipe',$mode = 0666)
  14.     {
  15.         $fifoPath= "/tmp/$name.". posix_getpid();
  16.         if (!file_exists($fifoPath)){
  17.             if (!posix_mkfifo($fifoPath,$mode)){
  18.                 error("create new pipe ($name) error.");
  19.                 return false;
  20.             }
  21.         } else{
  22.             error( "pipe ($name) has exit.");
  23.             return false;
  24.         }
  25.         $this->fifoPath= $fifoPath;
  26.     }
  27.    
  28. ///////////////////////////////////////////////////

  29. // 写管道函数开始

  30. ///////////////////////////////////////////////////

  31.     function open_write()
  32.     {
  33.         $this->w_pipe= fopen($this->fifoPath,'w');
  34.         if ($this->w_pipe== NULL) {
  35.             error("open pipe {$this->fifoPath} for write error.");
  36.             return false;
  37.         }
  38.         return true;
  39.     }

  40.     function write($data)
  41.     {
  42.         return fwrite($this->w_pipe,$data);
  43.     }

  44.     function write_all($data)
  45.     {
  46.         $w_pipe= fopen($this->fifoPath,'w');
  47.         fwrite($w_pipe,$data);
  48.         fclose($w_pipe);
  49.     }

  50.     function close_write()
  51.     {
  52.         return fclose($this->w_pipe);
  53.     }
  54. /////////////////////////////////////////////////////////

  55. /// 读管道相关函数开始

  56. ////////////////////////////////////////////////////////

  57.     function open_read()
  58.     {
  59.         $this->r_pipe= fopen($this->fifoPath,'r');
  60.         if ($this->r_pipe== NULL) {
  61.             error("open pipe {$this->fifoPath} for read error.");
  62.             return false;
  63.         }
  64.         return true;
  65.     }

  66.     function read($byte= 1024)
  67.     {
  68.         return fread($this->r_pipe,$byte);
  69.     }

  70.     function read_all()
  71.     {
  72.         $r_pipe= fopen($this->fifoPath,'r');
  73.         $data= '';
  74.         while (!feof($r_pipe)){
  75.             //echo "read one K\n";

  76.             $data.= fread($r_pipe, 1024);
  77.         }
  78.         fclose($r_pipe);
  79.         return $data;
  80.     }

  81.     function close_read()
  82.     {
  83.         return fclose($this->r_pipe);
  84.     }
  85. ////////////////////////////////////////////////////

  86.     /**
  87.      * 删除管道
  88.      *
  89.      * @return boolean is success
  90.      */
  91.     function rm_pipe()
  92.     {
  93.         return unlink($this->fifoPath);
  94.     }
  95. }
  96. ?>

有了这个类,就可以实现简单的管道通信了,因为这个教程是多线程编程系列教程的一个部分。

这个管道类的应用部分,将放到第三部分。

 

PHP多线程编程(三)多线程抓取网页的演示

要理解这个部分的代码,请阅读:

用 Socket 和 Pcntl 实现一个多线程服务器(一)

PHP多线程编程(一)

PHP多线程编程(二)管道通信

 

我们知道,从父进程到子经常的数据传递相对比较容易一些,但是从子进程传递到父进程就比较的困难。

有很多办法实现进程交互,在php中比较方便的是 管道通信。当然,还可以通过 socket_pair 进行通信。

 

首先是服务器为了应对每一个请求要做的事情(发送一个url 序列,url序列用\t 分割。而结束标记是 \n)

  1. function clientHandle($msgsock, $obj)
  2. {
  3.     $nbuf = '';
  4.     socket_set_block($msgsock);
  5.     do {
  6.         if (false === ($buf =@socket_read($msgsock, 2048, PHP_NORMAL_READ))){
  7.             $obj->error("socket_read() failed: reason: ". socket_strerror(socket_last_error($msgsock)));
  8.             break;
  9.         }
  10.         $nbuf .= $buf;

  11.         if (substr($nbuf,-1) != "\n"){
  12.             continue;
  13.         }
  14.         $nbuf = trim($nbuf);
  15.         if ($nbuf== 'quit') {
  16.             break;
  17.         }
  18.         if ($nbuf== 'shutdown') {
  19.             break;
  20.         }
  21.         $url = explode("\t", $nbuf);
  22.         $nbuf = '';

  23.         $talkback = serialize(read_ntitle($url));
  24.         socket_write($msgsock, $talkback, strlen($talkback));
  25.         debug("write to the client\n");
  26.         break;
  27.     } while (true);
  28. }

上面代码比较关键的一个部分是 read_ntitle,这个函数实现多线程的读取标题。

 

代码如下:(为每一个url fork 一个线程,然后打开管道 ,读取到的标题写入到管道里面去,主线程一直的在读取管道数据,直到所有的数据读取完毕,最后删除管道)

  1. function read_ntitle($arr)
  2. {
  3.     $pipe = new Pipe("multi-read");
  4.     foreach ($arr as $k=> $item)
  5.     {
  6.         $pids[$k]= pcntl_fork();
  7.         if(!$pids[$k])
  8.         {
  9.              $pipe->open_write();
  10.              $pid = posix_getpid();
  11.              $content = base64_encode(read_title($item));
  12.              $pipe->write("$k,$content\n");
  13.              $pipe->close_write();
  14.              debug("$k: write success!\n");
  15.              exit;
  16.         }
  17.     }
  18.     debug("read begin!\n");
  19.     $data = $pipe->read_all();
  20.     debug("read end!\n");

  21.     $pipe->rm_pipe();
  22.     return parse_data($data);
  23. }
  24. parse_data 代码如下,非常的简单,就不说了。
  25. function parse_data($data)
  26. {
  27.     $data = explode("\n", $data);
  28.     $new = array();
  29.     foreach ($data as $value)
  30.     {
  31.         $value = explode(",", $value);
  32.         if (count($value)== 2){
  33.             $value[1]= base64_decode($value[1]);
  34.             $new[intval($value[0])]= $value[1];
  35.         }
  36.     }
  37.     ksort($new, SORT_NUMERIC);
  38.     return $new;
  39. }

上面代码中,还有一个函数read_title 比较有技巧。为了兼容性,我没有采用curl,而是直接采用socket 通信。

在下载到 title 标签后,就停止读取内容,以节省时间。代码如下:

  1. function read_title($url)
  2. {
  3.     $url_info = parse_url($url);
  4.     if (!isset($url_info['host'])|| !isset($url_info['scheme'])){
  5.      return false;
  6.     }
  7.     $host = $url_info['host'];
  8.     
  9.  $port = isset($url_info['port'])? $url_info['port']: null;
  10.  $path = isset($url_info['path'])? $url_info['path']: "/";
  11.  if(isset($url_info['query'])) $path .= "?".$url_info['query'];
  12.  if(empty($port)){
  13.   $port = 80;
  14.  }
  15.  if ($url_info['scheme']== 'https'){
  16.   $port = 443;
  17.  }
  18.  if ($url_info['scheme']== 'http') {
  19.   $port = 80;
  20.  }
  21.     $out = "GET $path HTTP/1.1\r\n";
  22.     $out .="Host: $host\r\n";
  23.     $out .="User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.7)\r\n";
  24.     $out .="Connection: Close\r\n\r\n";
  25.     $fp = fsockopen($host, $port, $errno, $errstr, 5);
  26.     if ($fp== NULL){
  27.      error("get title from $url, error. $errno: $errstr \n");
  28.      return false;
  29.     }
  30.     fwrite($fp, $out);
  31.     $content ='';
  32.     while (!feof($fp)){
  33.         $content .= fgets($fp, 1024);
  34.         if (preg_match("/<title>(.*?)<\/title>/is", $content, $matches)){
  35.              fclose($fp);
  36.             return encode_to_utf8($matches[1]);
  37.         }
  38.     }
  39.     fclose($fp);
  40.     return false;
  41. }

  42. function encode_to_utf8($string)
  43. {
  44.      return mb_convert_encoding($string,"UTF-8", mb_detect_encoding($string,"UTF-8, GB2312, ISO-8859-1", true));
  45. }

这里,我只是检测了 三种最常见的编码。

其他的代码都很简单,这些代码都是测试用的,如果你要做这样一个服务器,一定要进行优化处理。特别是,要防止一次打开太多的线程,你要做更多的处理。

很多时候,我们抱怨php 不支持多线程,实际上,php是支持多线程的。当然,没有那么多的进程通信的选项,而多线程的核心就在于线程的通信与同步。

在web开发中,这样的多线程基本上是不会使用的,因为有很严重的性能问题。要实现比较简单的多线程,高负载,必须借助其扩展。

 

PHP多进程(四) 内部多进程

上面一个系列的教程:

用 Socket 和 Pcntl 实现一个多进程服务器(一)

PHP多进程编程(一)

PHP多进程编程(二)管道通信

PHP多进程编程(三)多进程抓取网页的演示

 

说的都是只兼容unix 服务器的多进程,下面来讲讲在window 和 unix 都兼容的多进程(这里是泛指,下面的curl实际上是通过IO复用实现的)。

    通过扩展实现多线程的典型例子是CURL,CURL 支持多线程的抓取网页的功能。

这部分过于抽象,所以,我先给出一个CURL并行抓取多个网页内容的一个分装类。这个类实际上很实用,

详细分析这些函数的内部实现将在下一个教程里面描述。

    你可能不能很好的理解这个类,而且,php curl 官方主页上都有很多错误的例子,在讲述了其内部机制

后,你就能够明白了。

    先看代码:

  1. <?php
  2. class Http_MultiRequest
  3. {
  4.     //要并行抓取的url 列表

  5.     private $urls= array();

  6.     //curl 的选项

  7.     private $options;
  8.     
  9.     //构造函数

  10.     function __construct($options= array())
  11.     {
  12.         $this->setOptions($options);
  13.     }

  14.     //设置url 列表

  15.     function setUrls($urls)
  16.     {
  17.         $this->urls= $urls;
  18.         return $this;
  19.     }


  20.     //设置选项

  21.     function setOptions($options)
  22.     {
  23.         $options[CURLOPT_RETURNTRANSFER]= 1;
  24.         if (isset($options['HTTP_POST']))
  25.         {
  26.             curl_setopt($ch, CURLOPT_POST, 1);
  27.             curl_setopt($ch, CURLOPT_POSTFIELDS,$options['HTTP_POST']);
  28.             unset($options['HTTP_POST']);
  29.         }

  30.         if (!isset($options[CURLOPT_USERAGENT]))
  31.         {
  32.             $options[CURLOPT_USERAGENT]= 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1;)';
  33.         }

  34.         if (!isset($options[CURLOPT_FOLLOWLOCATION]))
  35.         {
  36.             $options[CURLOPT_FOLLOWLOCATION]= 1;
  37.         }

  38.         if (!isset($options[CURLOPT_HEADER]))
  39.         {
  40.             $options[CURLOPT_HEADER]= 0;
  41.         }
  42.         $this->options= $options;
  43.     }

  44.     //并行抓取所有的内容

  45.     function exec()
  46.     {
  47.         if(empty($this->urls)|| !is_array($this->urls))
  48.         {
  49.             return false;
  50.         }
  51.         $curl= $data= array();
  52.         $mh= curl_multi_init();
  53.         foreach($this->urlsas $k=> $v)
  54.         {
  55.             $curl[$k]= $this->addHandle($mh,$v);
  56.         }
  57.         $this->execMulitHandle($mh);
  58.         foreach($this->urlsas $k=> $v)
  59.         {
  60.             $data[$k]= curl_multi_getcontent($curl[$k]);
  61.             curl_multi_remove_handle($mh,$curl[$k]);
  62.         }
  63.         curl_multi_close($mh);
  64.         return $data;
  65.     }
  66.     
  67.     //只抓取一个网页的内容。

  68.     function execOne($url)
  69.     {
  70.         if (empty($url)){
  71.             return false;
  72.         }
  73.         $ch= curl_init($url);
  74.         $this->setOneOption($ch);
  75.         $content= curl_exec($ch);
  76.         curl_close($ch);
  77.         return $content;
  78.     }
  79.     
  80.     //内部函数,设置某个handle 的选项

  81.     private function setOneOption($ch)
  82.     {
  83.         curl_setopt_array($ch,$this->options);
  84.     }

  85.     //添加一个新的并行抓取 handle

  86.     private function addHandle($mh,$url)
  87.     {
  88.         $ch= curl_init($url);
  89.         $this->setOneOption($ch);
  90.         curl_multi_add_handle($mh,$ch);
  91.         return $ch;
  92.     }

  93.     //并行执行(这样的写法是一个常见的错误,我这里还是采用这样的写法,这个写法

  94.     //下载一个小文件都可能导致cup占用100%, 并且,这个循环会运行10万次以上

  95.     //这是一个典型的不懂原理产生的错误。这个错误在PHP官方的文档上都相当的常见。)

  96.     private function execMulitHandle($mh)
  97.     {
  98.         $running= null;
  99.         do {
  100.             curl_multi_exec($mh,$running);
  101.         } while($running> 0);
  102.     }
  103. }

看最后一个注释最多的函数,这个错误在平时调试的时候可能不太容易发现,因为程序完全正常,但是,在生产服务器下,马上会引起崩溃效果。

解释为什么不能这样,必须从C 语言内部实现的角度来分析。这个部分将放到下一个教程(PHP高级编程之--单线程实现并行抓取网页 )。不过不是通过C语言来表述原理,而是通过PHP

    这个类,实际上也就很简单的实现了前面我们费了4个教程的篇幅,并且是九牛二虎之力才实现的多线程的抓取网页的功能。在纯PHP的实现下,我们只能用一个后台服务的方式来比较好的实现,但是当你使用 操作系统接口语言 C 语言时候,这个实现当然就更加的简单,灵活,高效。

    就同时抓取几个网页这样一件简单的事情,实际上在底层涉及到了很多东西,对很多半路出家的PHP程序员,可能不喜欢谈多线程这个东西,深入了就涉及到操作系统,浅点说就是并行运行好几个“程序”。但是,很多时候,多线程必不可少,比如要写个快点的爬虫,往往就会浪费九牛二虎之力。不过,PHP的程序员现在应该感谢CURL 这个扩展,这样,你完全不需要用你不太精通的 python 去写爬虫了,对于一个中型大小的爬虫,有这个内部多线程,就已经足够了。

 

最后是上面的类的一个测试的例子:

  1. $urls = array("http://baidu.com","http://baidu.com","http://baidu.com","http://baidu.com","http://baidu.com","http://baidu.com","http://www.google.com","http://www.sina.com.cn",);
  2. $m = new Http_MultiRequest();

  3. $t = microtime(true);
  4. $m->setUrls($urls);

  5. //parallel fetch(并行抓取):
  6. $data = $m->exec();
  7. $parallel_time = microtime(true)- $t;
  8. echo $parallel_time . "\n";

  9. $t = microtime(true);

  10. //serial fetch(串行抓取):
  11. foreach ($urls as $url)
  12. {
  13.     $data[]= $m->execOne($url);
  14. }
  15. $serial_time = microtime(true)- $t;
  16. echo $serial_time . "\n";

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 快递未收到货能退款商家拒绝怎么办 黑色牛仔裤有一块洗白了怎么办 黑色牛仔裤被洗衣液烧了怎么办 在蘑菇街退鞋子商家非说脏了怎么办 牛皮屑怎么办ke靠成都银康 微信订阅号取消关注之后还在怎么办 腾讯新闻红包领取说帐号异常怎么办 计算机职弥报名没选模块怎么办 有人用我手机注册有赞了怎么办 一件代发别人的货被投诉了怎么办 淘宝极速退款后商家拒收快递怎么办 运费险退到天猫垫付账户了怎么办 淘宝卖家食品有问题该怎么办 美团外卖不要辣椒给放了怎么办 旺旺卖家拒收我的消息怎么办 淘宝清空购物车大奖到上限了怎么办 游戏无响应除了退出还能怎么办 淘宝给差评了卖家一直打电话怎么办 电脑说带宽问题无法观看视频怎么办 手机淘宝上的购买信息删除了怎么办 为什么支付宝有钱淘宝付不了怎么办 苹果平板电脑上的淘宝点不开怎么办 淘宝付了两次款只有一个订单怎么办 淘宝付款显示支付宝账号异常怎么办 手机老卡换新卡淘宝付不了款怎么办 淘宝买东西退款卖家拒绝退款怎么办 淘宝店铺收藏图片怎么点不了怎么办 手机淘宝显示用户被限制登录怎么办 淘宝设置登录密码原密码忘了怎么办 斑马智行淘宝号换没法登录了怎么办 淘宝卖家手机版显示宝贝不全怎么办 在电脑上登的淘宝账号退不了怎么办 淘宝买家退款不退货写假货怎么办 淘宝卖家已发布商品没货了怎么办 京东换货附近没有京东自提点怎么办 一直显示手机淘宝已停止运行怎么办 唯品会买了不可以退货的衣服怎么办 淘宝店卖东西邮费太贵怎么办 支付宝登录上去必须手机验证怎么办 支付宝里的钱被盗了怎么办 淘宝绑定的支付宝账号忘记了怎么办