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 ...
随机推荐
- 杂项:ASP.NET Web API
ylbtech-杂项:ASP.NET Web API ASP.NET Web API 是一种框架,用于轻松构建可以访问多种客户端(包括浏览器和移动设备)的 HTTP 服务. ASP.NET Web A ...
- thinkphp 日志记录
日志记录\ThinkPHP\Lib\Think\Core\Log.class.php 1.可以在config.php中进行设置,默认为关闭状态. 'APP_DEBUG' => true 打开\T ...
- yii widget使用的3个用法
yii视图中使用的widget方式总结:常用的有3种方式:一.显示详细信息: $this->widget('zii.widgets.CDetailView', array( 'data' =&g ...
- bzoj1705[Usaco2007 Nov]Telephone Wire 架设电话线(dp优化)
1705: [Usaco2007 Nov]Telephone Wire 架设电话线 Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 441 Solved: ...
- Spring Cloud (2) 服务消费者-基础
LoadBalancerClient 使用Spring Cloud提供的负载均衡器客户端来实现服务的消费. 首先创建一个服务消费者工程,命名为com.david.consumer,并在pom.xml中 ...
- 5.20 mybatis反向生成的映射文件xml(如果需要自己定义其他sql语句时如下)
解决mybatis-generator 生成的mapper.xml覆盖自定义sql的问题 mybatis-generator是个好工具,一建即可生成基本增删改成功能的mapper.xml.但这些是不够 ...
- Android HTTP下载文件并保存到本地或SD卡
想把文件保存到SD卡中,一定要知道SD卡的路径,获取SD卡路径: Environment.getExternalStorageDirectory() 另外,在保存之前要判断SD卡是否已经安装好,并且可 ...
- dubbo之结果缓存
结果缓存,用于加速热门数据的访问速度,Dubbo提供声明式缓存,以减少用户加缓存的工作量. lru 基于最近最少使用原则删除多余缓存,保持最热的数据被缓存. threadlocal 当前线程缓存,比如 ...
- 解决Fiddler抓包上不了网的问题:
以前安装Fiddler 没有配置过相关设置,经常出现就是打开fiddler后,浏览器就无法上网了,刚开始觉得可能是因为而公司上网是需要自己的代理的,但fiddler打开后默认127.0.0.1作为IE ...
- 静态修改url,不跳转
history.replaceState(null,document.title,'www.baidu.com');