php模拟并发
原文: http://blog.csdn.net/zhang_xinglong/article/details/16339867
-------------------------------------------------------------------------------------------------------------------------------
并发请求理论描述:假设有一个client,程序逻辑是要请求三个不同的server,处理各自的响应。传统模型当然是顺序执行,先发送第一个请求,等待收到响应数据后再发送第二个请求,以此类推。就像是单核CPU,一次只能处理一件事,其他事情被暂时阻塞。而并发模式可以让三个server同时处理各自请求,这就可以使大量时间复用。
画个图更好说明问题:

前者为阻塞模式,忽略请求响应等时间,总耗时为700ms;而后者非阻塞模式,由于三个请求可以同时得到处理,总耗时只有300ms。所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回。所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。
PHP本身是不支持多线程的,但是它可以利用Linux和apache的多线程能力。php模拟的多线程其实只是多进程,并不是真正的多线程。以下是几种php模拟多线程的方法:
1.php+shell (利用linux os)
php代码(test.php):
- <?php
 - for($i = 0; $i < 10; $i++)
 - {
 - echo $i;
 - sleep(5); //这里为了方便看效果sleep一下让脚本执行时间更长
 - }
 - ?>
 
shell代码(test.sh):
- #!/bin/bash
 - for i in 1 2 3 4 5
 - do
 - /usr/bin/php -r -q test.php &
 - done
 
注意:
在请求php代码的那行末尾有一个&符号,这个是关键,不加的话是不能进行多线程的,&表示将服务推送到后台执行,因此在shell的每次的循环中不必等php的代码全部执行完在请求下一个文件,而是同时进行的,这样就实现了多线程,下面运行下shell看下效果,这里你将看到5个test.php进程,再利用linux的定时器,定时请求这个shell,在处理一些需要多线程的任务,例如,批量下载时,非常好用!
参考:http://blog.csdn.net/tianmohust/article/details/8208627
2.php+pcntl(利用linux os)
只能用在Unix Like OS,Windows不可用。且推荐仅仅在CLI模式运行,不要在WEB服务器环境运行。
- <?php
 - declare(ticks=1);
 - //是否等待进程结束
 - $bWaitFlag = FALSE;
 - //进程总数
 - $intNum = 10;
 - //进程PID数组
 - $pids = array();
 - echo ("Start\n");
 - for($i = 0; $i < $intNum; $i++)
 - {
 - //产生子进程,而且从当前行之下开试运行代码,而且不继承父进程的数据信息
 - $pids[$i] = pcntl_fork();
 - if( ! $pids[$i])
 - {
 - //子进程进程代码段_Start
 - $str = "";
 - sleep(5+$i);
 - for ($j = 0; $j < $i; $j++)
 - {
 - $str .= "*";
 - }
 - echo "$i -> " . time() . " $str \n";
 - exit();
 - //子进程进程代码段_End
 - }
 - }
 - if ($bWaitFlag)
 - {
 - for($i = 0; $i < $intNum; $i++)
 - {
 - pcntl_waitpid($pids[$i], $status, WUNTRACED);
 - echo "wait $i -> " . time() . "\n";
 - }
 - }
 - echo ("End\n");
 - ?>
 
运行结果如下:
- [qiao@oicq qiao]$ php test.php
 - Start
 - End
 - [qiao@oicq qiao]$ ps -aux | grep "php"
 - qiao 32275 0.0 0.5 49668 6148 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32276 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32277 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32278 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32279 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32280 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32281 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32282 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32283 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32284 0.0 0.5 49668 6152 pts/1 S 14:03 0:00 /usr/local/php4/b
 - qiao 32286 0.0 0.0 1620 600 pts/1 S 14:03 0:00 grep php
 - [qiao@oicq qiao]$ 0 -> 1133503401
 - 1 -> 1133503402 *
 - 2 -> 1133503403 **
 - 3 -> 1133503404 ***
 - 4 -> 1133503405 ****
 - 5 -> 1133503406 *****
 - 6 -> 1133503407 ******
 - 7 -> 1133503408 *******
 - 8 -> 1133503409 ********
 - 9 -> 1133503410 *********
 - [qiao@oicq qiao]$
 
如果$bWaitFlag=TURE,则结果如下:
- [qiao@oicq qiao]$ php test.php
 - Start
 - 0 -> 1133503602
 - wait 0 -> 1133503602
 - 1 -> 1133503603 *
 - wait 1 -> 1133503603
 - 2 -> 1133503604 **
 - wait 2 -> 1133503604
 - 3 -> 1133503605 ***
 - wait 3 -> 1133503605
 - 4 -> 1133503606 ****
 - wait 4 -> 1133503606
 - 5 -> 1133503607 *****
 - wait 5 -> 1133503607
 - 6 -> 1133503608 ******
 - wait 6 -> 1133503608
 - 7 -> 1133503609 *******
 - wait 7 -> 1133503609
 - 8 -> 1133503610 ********
 - wait 8 -> 1133503610
 - 9 -> 1133503611 *********
 - wait 9 -> 1133503611
 - End
 - [qiao@oicq qiao]$
 
从多进程的例子可以看出,使用pcntl_fork()之后,将生成一个子进程,而且子进程运行的代码,从pcntl_fork()之后的代码开始,而子进程不继承父进程的数据信息(实际上是把父进程的数据做了一个全新的拷贝),因而使用if(!$pids[$i]) 来控制子进程实际运行的代码段。
参考:http://hi.baidu.com/tangyubinsir/item/43c04f85ea7709d4d1f8cd84和http://www.itlearner.com/article/4908
3.php+pthreads
参考:http://blog.csdn.net/leinchu/article/details/11795985
4.php+socket(利用web server)
假设你要建立一个服务来检查正在运行的n台服务器,以确定他们还在正常运转。你可能会写下面这样的代码:
- <?php
 - $hosts = array("www.baidu.com", "www.sohu.com", "www.163.com");
 - $timeout = 15;
 - $status = array();
 - foreach ($hosts as $host)
 - {
 - $errno = 0;
 - $errstr = "";
 - $s = fsockopen($host, 80, $errno, $errstr, $timeout);
 - if ($s)
 - {
 - $status[$host] = "Connected\n";
 - fwrite($s, "HEAD / HTTP/1.0\r\nHost: $host\r\n\r\n"); //第二个参数是HTTP协议中规定的请求头,不明白的请看RFC中的定义
 - do
 - {
 - $data = fread($s, 8192);
 - if (strlen($data) == 0)
 - {
 - break;
 - }
 - $status[$host] .= $data; //返回连接状态
 - }
 - while (true);
 - fclose($s);
 - }
 - else
 - {
 - $status[$host] = "Connection failed: $errno $errstrn";
 - }
 - }
 - echo '<pre>';
 - print_r($status);
 - ?>
 
它运行的很好,但是在fsockopen()分析完hostname并且建立一个成功的连接(或者延时$timeout秒)之前,扩充这段代码来管理大量服务器将耗费很长时间。
因此我们必须放弃这段代码;我们可以建立异步连接-不需要等待fsockopen返回连接状态。PHP仍然需要解析hostname(所以直接使用ip更加明智),不过将在打开一个连接之后立刻返回,继而我们就可以连接下一台服务器。
有两种方法可以实现;PHP5中可以使用新增的stream_socket_client()函数直接替换掉fsocketopen()。PHP5之前的版本,你需要自己动手,用sockets扩展解决问题。
下面是PHP5中的解决方法:
- <?php
 - $hosts = array("www.baidu.com", "www.sohu.com", "www.163.com");
 - $timeout = 15;
 - $status = array();
 - $sockets = array();
 - /* Initiate connections to all the hosts simultaneously */
 - foreach ($hosts as $id => $host)
 - {
 - $s = stream_socket_client(
 - "$host:80", $errno, $errstr, $timeout,
 - TREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT);
 - /* 这里需要稍微延迟一下,否则下面fwrite中的socket句柄不一定能真正使用
 - * 这里应该是PHP的一处bug,查了一下,官方bug早在08年就有人提交了
 - * 我的5.2.8中尚未解决,不知最新的5.3中是否修正
 - */
 - usleep(10);
 - if ($s)
 - {
 - $sockets[$id] = $s;
 - $status[$hosts[$id]] = "in progress";
 - }
 - else
 - {
 - $status[$hosts[$id]] = "failed, $errno $errstr";
 - }
 - }
 - /* Now, wait for the results to come back in */
 - while (count($sockets))
 - {
 - $read = $write = $sockets;
 - // $e = null;
 - /* This is the magic function - explained below */
 - $n = stream_select($read, $write, $e, $timeout);
 - if ($n > 0) //据说stream_select返回值不总是可信任的
 - // if (count($read))
 - {
 - /* readable sockets either have data for us, or are failed connection attempts */
 - foreach ($read as $r)
 - {
 - /* stream_select generally shuffles $read, so we need to
 - compute from which socket(s) we're reading. */
 - $id = array_search($r, $sockets);
 - $data = fread($r, 8192);
 - /* A socket is readable either because it has
 - data to read, OR because it's at EOF. */
 - if (strlen($data) == 0)
 - {
 - if ($status[$hosts[$id]] == "in progress")
 - {
 - $status[$hosts[$id]] = "failed to connect";
 - }
 - fclose($r);
 - unset($sockets[$id]);
 - }
 - else
 - {
 - $status[$hosts[$id]] = $data;
 - }
 - }
 - /* writeable sockets can accept an HTTP request */
 - foreach ($write as $w)
 - {
 - $id = array_search($w, $sockets);
 - if(is_resource($w) && feof($w) === FALSE)
 - {
 - @fwrite($w, "HEAD / HTTP/1.0\r\nHost: " . $hosts[$id] . "\r\n\r\n");
 - // $flag && $status[$hosts[$id]] = "waiting for response";
 - }
 - }
 - }
 - else
 - {
 - /* timed out waiting; assume that all hosts associated with $sockets are faulty */
 - foreach ($sockets as $id => $s)
 - {
 - $status[$hosts[$id]] = "timed out " . $status[$hosts[$id]];
 - }
 - break;
 - }
 - }
 - echo '<pre>';var_dump($status);
 - ?>
 
我们用stream_select()等待sockets打开的连接事件。stream_select()调用系统的select()函数来工作:前面三个参数是你要使用的streams的数组;你可以对其读取,写入和获取异常(分别针对三个参数)。stream_select()可以通过设置$timeout(秒)参数来等待事件发生-事件发生时,相应的sockets数据将写入你传入的参数。
下面是PHP4.1.0之后版本的实现,如果你已经在编译PHP时包含了sockets(ext/sockets)支持,你可以使用根上面类似的代 码,只是需要将上面的streams/filesystem函数的功能用ext/sockets函数实现。主要的不同在于我们用下面的函数代替 stream_socket_client()来建立连接:
- <?php
 - // This value is correct for Linux, other systems have other values
 - define('EINPROGRESS', 115);
 - function non_blocking_connect($host, $port, &$errno, &$errstr, $timeout) {
 - $ip = gethostbyname($host);
 - $s = socket_create(AF_INET, SOCK_STREAM, 0);
 - if (socket_set_nonblock($s)) {
 - $r = @socket_connect($s, $ip, $port);
 - if ($r || socket_last_error() == EINPROGRESS) {
 - $errno = EINPROGRESS;
 - return $s;
 - }
 - }
 - $errno = socket_last_error($s);
 - $errstr = socket_strerror($errno);
 - socket_close($s);
 - return false;
 - }
 - ?>
 
现在用socket_select()替换掉stream_select(),用socket_read()替换掉fread(),用socket_write()替换掉fwrite(),用socket_close()替换掉fclose()就可以执行脚本了! PHP5的先进之处在于,你可以用stream_select()处理几乎所有的stream。例如你可以通过include STDIN用它接收键盘输入并保存进数组,你还可以接收通过proc_open()打开的管道中的数据。
注:select在socket编程中还是比较重要的,可是对于初学socket的人来说都不太爱用select写程序,他们只是习惯写诸如connect、 accept、recv或recvfrom这样的阻塞程序。可是使用select就可以完成非阻塞方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
参考:http://blog.csdn.net/21aspnet/article/details/7420024
5.php+curl
(1)经典curl并发机制和存在问题
经典的cURL实现机制在网上很容易找到, 比如参考PHP在线手册的如下实现方式:
- <?php
 - function classic_curl($urls, $delay) {
 - $queue = curl_multi_init();
 - $map = array();
 - foreach ($urls as $url) {
 - // create cURL resources
 - $ch = curl_init();
 - // set URL and other appropriate options
 - curl_setopt($ch, CURLOPT_URL, $url);
 - curl_setopt($ch, CURLOPT_TIMEOUT, 1);
 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 - curl_setopt($ch, CURLOPT_HEADER, 0);
 - curl_setopt($ch, CURLOPT_NOSIGNAL, true);
 - // add handle
 - curl_multi_add_handle($queue, $ch);
 - $map[$url] = $ch;
 - }
 - $active = null;
 - // execute the handles
 - do {
 - $mrc = curl_multi_exec($queue, $active);
 - } while ($mrc == CURLM_CALL_MULTI_PERFORM);
 - while ($active > 0 && $mrc == CURLM_OK) {
 - if (curl_multi_select($queue, 0.5) != -1) {
 - do {
 - $mrc = curl_multi_exec($queue, $active);
 - } while ($mrc == CURLM_CALL_MULTI_PERFORM);
 - }
 - }
 - $responses = array();
 - foreach ($map as $url=>$ch) {
 - $responses[$url] = callback(curl_multi_getcontent($ch), $delay);
 - curl_multi_remove_handle($queue, $ch);
 - curl_close($ch);
 - }
 - curl_multi_close($queue);
 - return $responses;
 - }
 - ?>
 
首先将所有的URL压入并发队列, 然后执行并发过程, 等待所有请求接收完之后进行数据的解析等后续处理. 在实际的处理过程中, 受网络传输的影响, 部分URL的内容会优先于其他URL返回, 但是经典cURL并发必须等待最慢的那个URL返回之后才开始处理, 等待也就意味着CPU的空闲和浪费. 如果URL队列很短, 这种空闲和浪费还处在可接受的范围, 但如果队列很长, 这种等待和浪费将变得不可接受.
(2)改进的rolling curl并发方式
仔细分析不难发现经典cURL并发还存在优化的空间, 优化的方式时当某个URL请求完毕之后尽可能快的去处理它, 边处理边等待其他的URL返回, 而不是等待那个最慢的接口返回之后才开始处理等工作, 从而避免CPU的空闲和浪费. 闲话不多说, 下面贴上具体的实现:
- <?php
 - function rolling_curl($urls, $delay) {
 - $queue = curl_multi_init();
 - $map = array();
 - foreach ($urls as $url) {
 - $ch = curl_init();
 - curl_setopt($ch, CURLOPT_URL, $url);
 - curl_setopt($ch, CURLOPT_TIMEOUT, 1);
 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 - curl_setopt($ch, CURLOPT_HEADER, 0);
 - curl_setopt($ch, CURLOPT_NOSIGNAL, true);
 - curl_multi_add_handle($queue, $ch);
 - $map[(string) $ch] = $url;
 - }
 - $responses = array();
 - do {
 - while (($code = curl_multi_exec($queue, $active)) == CURLM_CALL_MULTI_PERFORM) ;
 - if ($code != CURLM_OK) { break; }
 - // a request was just completed -- find out which one
 - while ($done = curl_multi_info_read($queue)) {
 - // get the info and content returned on the request
 - $info = curl_getinfo($done['handle']);
 - $error = curl_error($done['handle']);
 - $results = callback(curl_multi_getcontent($done['handle']), $delay);
 - $responses[$map[(string) $done['handle']]] = compact('info', 'error', 'results');
 - // remove the curl handle that just completed
 - curl_multi_remove_handle($queue, $done['handle']);
 - curl_close($done['handle']);
 - }
 - // Block for data in / output; error handling is done by curl_multi_exec
 - if ($active > 0) {
 - curl_multi_select($queue, 0.5);
 - }
 - } while ($active);
 - curl_multi_close($queue);
 - return $responses;
 - }
 - ?>
 
(3)两种并发实现的性能对比
性能测试中用到的回调函数为:
- function callback($data, $delay) {
 - preg_match_all('/<h3>(.+)<\/h3>/iU', $data, $matches);
 - usleep($delay);
 - return compact('data', 'matches');
 - }
 
数据处理回调无延迟时: Rolling Curl略优, 但性能提升效果不明显.数据处理回调延迟5毫秒: Rolling Curl完胜, 性能提升40%左右.通过上面的性能对比, 在处理URL队列并发的应用场景中Rolling cURL应该是更加的选择, 并发量非常大(1000+)时, 可以控制并发队列的最大长度, 比如20, 每当1个URL返回并处理完毕之后立即加入1个尚未请求的URL到队列中, 这样写出来的代码会更加健壮, 不至于并发数太大而卡死或崩溃.
php模拟并发的更多相关文章
- C# 模拟并发
		
每次写博客,第一句话都是这样的:程序员很苦逼,除了会写程序,还得会写博客! 当然,题外话说多了,咱进入正题! 在处理大数据的时候,经常会发生并发,并发的情况发生后,会出现数据污读,从而产生脏数据. 首 ...
 - 代码轮子之很简单但是挺管用的基于C# Task的模拟并发的代码
		
代码轮子之很简单但是挺管用的基于C# Task的模拟并发的代码
 - Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就绪,挂起,运行) ,***协程概念,yield模拟并发(有缺陷),Greenlet模块(手动切换),Gevent(协程并发)
		
Python进阶----异步同步,阻塞非阻塞,线程池(进程池)的异步+回调机制实行并发, 线程队列(Queue, LifoQueue,PriorityQueue), 事件Event,线程的三个状态(就 ...
 - c# 模拟并发请求 ,只能并发2个连接。
		
使用 HttpWebRequest 模拟并发请求的时候,发现不管怎么提高thread 的数量,都没用,服务器端用计数器看到的都是2个连接,见下图(关于计数器怎么开,百度) 然后搜了一下,发现需要在ap ...
 - 用压测模拟并发、并发处理(synchronized,redis分布式锁)
		
使用工具:Apache an 测压命令: ab -n 100 -c 100 http://www.baidu.com -n代表模拟100个请求,-c代表模拟100个并发,相当于100个人同时访问 ab ...
 - 对tomcat7模拟并发请求及相关配置参数的含义
		
这里的并不是真正的并发请求,因为for循环是间隔10毫秒,并且线程初始化也需要时间的,到真正执行http请求的时刻是不确定的. tomcat 的运行状态可以在webapps下的manage项目查看, ...
 - 自定义ThreadPoolExecutor带Queue缓冲队列的线程池 + JMeter模拟并发下单请求
		
.原文:https://blog.csdn.net/u011677147/article/details/80271174 拓展: https://github.com/jwpttcg66/GameT ...
 - 在使用HttpClient做客户端调用一个API时 模拟并发调用时发生“死锁"?
		
平时还是比较喜欢看书的..但有时候遇到问题还是经常感到脑袋一蒙..智商果然是硬伤.. 同事发现了个问题,代码如下: class Program { static void Main(string[] ...
 - Java模拟并发
		
=========================one============================= public class Bingfa { public static void m ...
 
随机推荐
- python使用MySQLdb向mySQL批量插入数据的方法
			
该功能通过调用mySQLdb python库中的 cursor.executemany()函数完成批量处理. 今天用这个函数完成了批量插入 例程: def test_insertDB(options) ...
 - .net core 下Web API 技术栈
			
API文档工具:swagger https://www.cnblogs.com/suxinlcq/p/6757556.html https://www.cnblogs.com/danvic712/p/ ...
 - mygenerator().next() AttributeError: 'generator' object has no attribute 'next'
			
def mygenerator(): print ("start ...") yield 5 mygenerator() print ("mygenerator():&q ...
 - 将DataTable某一列的值整体赋值给 另一个DataTable
			
将 DataTable某一列的值,赋值给 另一个DataTable: DataSet _ds=bll.GetAllList(); //将要取其中一列 DataView view = _ds.Table ...
 - 理解list和vector的区别
			
原文:http://genwoxuevc.blog.51cto.com/1852984/503337 vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随机存取( ...
 - Android  studio 添加引用新建类库
			
1.新建一个工程包 2.修改AndroidManifest.xml 将AndroidManifest.xml 修改为 <manifest xmlns:android="http://s ...
 - 【Oracle】删除手工创建的数据库
			
众所周知,DBCA创建的数据库可以通过DBCA命令删除,但是手工创建的数据库却不能用此方式删除,下面给出删除方式: SQL> startup mount exclusive SQL> al ...
 - 实现Android-JNI本地C++调试
			
1. 原文链接:NDK单步调试方法 如有问题或者版权要求,请拜访原作者或者通知本人. 最近为了性能需求,开始搞JNI,白手起搞真心不容易.中间差点崩溃了好几次,最终总算得到一点心得. JN ...
 - 【C++】颜色的设置
			
1.改变整个控制台的颜色用 system("color 0A"); 其中color后面的0是背景色代号,A是前景色代号.各颜色代码如下: 0=黑色 1=蓝色 2=绿色 3=湖蓝色 ...
 - jquery的attr和prop
			
注意不同版本的attr和prop,attr适用于自定义dom值,prop适用于带有固有属性