Loading... # [PHP异步编程简述](https://blog.p2hp.com/archives/5055) 概述 异步编程,我们从字面上理解,可以理解为代码非同步执行的。异步编程可以归结为四种模式:回调、事件监听、发布/订阅、promise模式。我们最熟悉的两种模式是回调和事件监听,举两个最简单的javascript例子,一个ajax,一个点击事件的绑定: 1 2 3 $.getJSON("uri", params, function(result) { do_something_with_data(result); }); 1 2 3 $("#id").click(function(){ do_something_when_user_click_id(); }); 以上两个示例有一个共同的特点,就是把函数当做参数传递给另一个函数。被传递的函数可以被称作为闭包,闭包的执行取决于父函数何时调用它。 优势与劣势 异步编程具有以下优势: 解耦,你可以通过事件绑定,将复杂的业务逻辑分拆为多个事件处理逻辑 并发,结合非阻塞的IO,可以在单个进程(或线程)内实现对IO的并发访问;例如请求多个URL,读写多个文件等 效率,在没有事件机制的场景中,我们往往需要使用轮询的方式判断一个事件是否产生 异步编程的劣势: 异步编程的劣势其实很明显——回调嵌套。相信一部分人在写ajax的时候遇到过这样的场景: 1 2 3 4 5 6 7 $.getJSON("uri", params, function(result_1) { $.getJSON("uri", result_1, function(result_2) { $.getJSON("uri", result_2, function(result_3) { do_something_with_data(result_3); }); });; }); 这样的写法往往是因为数据的依赖问题,第二次ajax请求依赖于第一次请求的返回结果,第三次ajax依赖于第二次。这样就造成深层次的回调嵌套,代码的可读性急剧下降。虽然有一些框架能够通过一些模式解决这样的问题,然并卵,代码的可读性相比同步的写法依然差很多。 异步编程的另一个劣势就是编写和调试的过程更加复杂,有时候你不知道什么时候你的函数才会被调用,以及他们被调用的顺序。而我们更习惯同步串行的编程方式。 然而,我相信一旦你开始使用异步编程,你一定会喜欢上这种方式,因为他能够带给你更多的便利。 PHP异步编程概述 在php语言中,异步的使用并不像javascript中那么多,归其原因主要是php一般是在web环境下工作,接收请求->读取数据->生成页面,这看起来天生就是一个串行的过程;所以,在php中,异步并没有广泛使用。 在javascript中的4中异步编程模式,均可以在php中实现。 回调: 1 2 3 4 array_walk($arr, function($key, $value){ $value += 1; }); print_r($arr); 回调的方式,在大多情况下,代码仍然是顺序执行的(array_walk->print_r的顺序)。回调函数的意义在于被传递者可以调用回调函数对数据进行处理,这样的好处在于提供更好的扩展性和解耦。我们可以把这个回调函数理解为一个格式化器,处理相同的数据,当我传递一个json过滤器时,返回的结果可能是一个json压缩过的字符串,当我传递的是一个xml过滤器时,返回的结果可能是一个xml字符串(有点多态的思想)。 事件监听(定时器,时间事件): 1 2 3 4 5 6 7 8 $loop = React\EventLoop\Factory::create(); $loop->addPeriodicTimer(5, function () { $memory = memory_get_usage() / 1024; $formatted = number_format($memory, 3).'K'; echo "Current memory usage: {$formatted}\n"; }); $loop->run(); 事件监听在PHP中用的并不多,但并不是没有,例如pcntl_signal()监听操作系统信号,以及其他IO事件的监听等等。上面的示例是一个事件事件的侦听,每隔5s中,会执行一次回调函数。 在四种异步模式中,事件监听的应用是更有意义的。然我们看一个同步的例子,下面这段代码用于向百度和google(一个不存在的网站)发起请求,同步的编写写法是先去请求百度或者google,等待请求结束后再请求另一个: 1 2 3 $http = new HTTP(); echo $http->get('http://www.baidu.com'); echo $http->get('http://www.google.com'); 基于事件的处理方式可以是这样的: 1 2 3 4 5 6 7 $http = new HTTP(); $http->get('www.baidu.com'); $http->get('www.huyanping.cn'); $http->on('response', function($response){ echo $response . PHP_EOL; }); $http->run(); 异步的写法允许我们同时处理多个事务,谁先完成,就先去处理谁。一个简单的异步http客户端见:async-http-php PHP有很多扩展和包提供了这方面的支持: ext-libevent libevent扩展,基于libevent库,支持异步IO和时间事件 ext-event event扩展,支持异步IO和时间事件 ext-libev libev扩展,基于libev库,支持异步IO和时间事件 ext-eio eio扩展,基于eio库,支持磁盘异步操作 ext-swoole swoole扩展,支持异步IO和时间,方便编写异步socket服务器,推荐使用 package-react react包,提供了全面的异步编程方式,包括IO、时间事件、磁盘IO等等 package-workerman workerman包,类似swoole,php编写 发布/订阅: 1 2 3 4 5 $lookup = new nsqphp\Lookup\Nsqlookupd; $nsq = new nsqphp\nsqphp($lookup); $nsq->subscribe('mytopic', 'somechannel', function($msg) { echo $msg->getId() . "\n"; })->run(); promise: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function getJsonResult() { return queryApi() ->then( // Transform API results to an object function ($jsonResultString) { return json_decode($jsonResultString); }, // Transform API errors to an exception function ($jsonErrorString) { $object = json_decode($jsonErrorString); throw new ApiErrorException($object->errorMessage); } ); } // Here we provide no rejection handler. If the promise returned has been // rejected, the ApiErrorException will be thrown getJsonResult() ->done( // Consume transformed object function ($jsonResultObject) { // Do something with $jsonResultObject } ); promise模式的意义在于解耦,就在刚刚我们提到的异步回调嵌套的问题,可以通过promise解决。其原理是在每一次传递回调函数的过程中,你都会拿到一个promie对象,而这个对象有一个then方法,then方法仍然可以返回一个promise对象,通过传递promise对象可以实现把多层嵌套分离出来。具体的代码需要去研究一下源码才可以,有点难懂,PHP的promise推荐阅读:promise 异步的实现原理 异步的实现大多情况下少不了循环监听事件,例如我们上面看到$loop->run(),这里其实是一个死循环,监听到事件则调用相应的处理函数。如果你对pcntl熟悉,你一定知道declare(tick=1),其实它也是一种循环,含义是每执行tick行代码,则检查一次是否有尚未处理的信号。虽然会有一个阻塞的死循环(大多数情况下,declare属于特殊情况),但我们可以对多个事件进行监听处理,同时可以在某一个事件处理的过程中停止循环,这样就可以实现并发异步的IO访问,甚至更多。 一段伪代码如下: 1 2 3 4 5 6 7 8 9 10 $async = new Async(); $async->on('request', function($requset){ do_something_with($request); }); // 这里其实就是$loop->run()的核心代码 while(true){ $async->hasRequest() ? $async->callRequestCallback() : null; sleep(1); } 总结 整片文章其实并不够详细,充其量算是一篇介绍性的文章,算是我在异步编程方面的一次总结。异步编程的学习并不像学习一门语言或者设计模式那样简单,它要求我们改变传统的编程方式。而异步IO对于学习者要求也略高,首先你必须熟悉同步的IO操作,甚至你需要了解一些协议解析的内容。 希望上面的内容对于初学者有一些帮助。文中若有错误的地方,还望指正。 最后修改:2023 年 08 月 10 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏