概述
异步编程,我们从字面上理解,可以理解为代码非同步执行的。异步编程可以归结为四种模式:回调、事件监听、发布/订阅、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 ) { }, // 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操作,甚至你需要了解一些协议解析的内容。
希望上面的内容对于初学者有一些帮助。文中若有错误的地方,还望指正。