基本用法
PHP 5.5 引入了 Generator
,Generator
通过封装之后,可以作为协程来进行使用。
Hprose 也提供了对 Generator
的一个封装,并且跟 Promise
相结合之后,可以实现异步代码同步化。
让我们来看一个例子:
- use \Hprose\Future;
- use \Hprose\Http\Client;
- Future\co(function() {
- $test = new Client("http://hprose.com/example/");
- var_dump((yield $test->hello("hprose")));
- $a = $test->sum(1, 2, 3);
- $b = $test->sum(4, 5, 6);
- $c = $test->sum(7, 8, 9);
- var_dump((yield $test->sum($a, $b, $c)));
- var_dump((yield $test->hello("world")));
- });
Future\co
(也可以用 Promise\co
)就是一个协程封装函数。它的功能以协程的方式来执行生成器函数。该方法允许带入参数执行。
在上面的例子中,$test 是一个 Hprose 的异步 Http 客户端。Hprose 2.0 的默认客户端为异步客户端,而不是同步客户端,这一点与 1.x 不同。
所以 $test->hello
和 $test->sum
两个调用的返回值实际上是一个 promise
对象。而 yield
关键字在这里的作用就是,可以等待调用完成并返回 promise
所包含的值,如果 promise
的最后的状态为 REJECTED
,那么 yield
将抛出一个异常,异常的值为 promise
对象中的 reason
属性值。
在上面的调用中,$a
, $b
, $c
三个变量都是 promise
对象,而 $test->sum
可以直接接受 promise
参数作为调用参数,当 $a
, $b
, $c
三个 promise
对象的状态都变为 FULFILLED
状态时,$test-sum($a, $b, $c)
才会真正的开始调用。而获取 $a
,$b
,$c
的三个调用是异步并发执行的。
上面程序的执行结果为:
string(12) "Hello hprose"
int(45)
string(11) "Hello world"
从结果我们可以看出,co
函数和 yield
的结合可以很方便的让异步程序编写同步化。这也是 Hprose 2.0 最有特色的改进之一。
虽然上面用 Hprose 远程调用来举例,但是 co
函数所实现的协程不是只对 Hprose 远程调用有效,而是对任何返回 promise
的对象都有效。所以,即使你不使用 Hprose 远程调用,也可以使用 co
函数和 Promise 来进行异步代码的同步化编写。
多协程并发
我们前面说过,如果在同一个协程内进行远程调用,如果不加 yield 关键字,多个远程调用就是并发执行的。加上 yield 关键字,就会变成顺序执行。
那么当开两个或多个协程时,结果是什么样子呢?我们来看一个例子:
- use \Hprose\Future;
- use \Hprose\Http\Client;
- $test = new Client("http://hprose.com/example/");
- Future\co(function() use ($test) {
- for ($i = 0; $i < 5; $i++) {
- var_dump((yield $test->hello("1-" . $i)));
- }
- });
- Future\co(function() use ($test) {
- for ($i = 0; $i < 5; $i++) {
- var_dump((yield $test->hello("2-" . $i)));
- }
- });
我们运行该程序之后,可以看到如下结果:
string(9) "Hello 2-0"
string(9) "Hello 1-0"
string(9) "Hello 1-1"
string(9) "Hello 2-1"
string(9) "Hello 2-2"
string(9) "Hello 1-2"
string(9) "Hello 1-3"
string(9) "Hello 2-3"
string(9) "Hello 2-4"
string(9) "Hello 1-4"
这个运行结果并不唯一,我们有可能看到不同顺序的输出,但是有一点可以保证,就是 Hello-1-X
中的 X
是按照顺序输出的,而 Hello-2-Y
中的 Y
也是按照顺序输出的。
也就是说,每个协程内的语句是按照顺序执行的,而两个协程确是并行执行的。
不过有一点要注意,上面的例子跟第一个例子有一点不同,那就是我们把 $test 客户端的创建拿到了协程外面。
如果像第一个例子那样放在协程中,我们就看不到这样的并发执行结果了。原因是,这里的每个客户端都有一个自己独立的事件循环,只有当一个事件循环执行完之后,才会执行另一个事件循环。
但如果换成 Hprose 的 Swoole 的客户端,则不管是放在里面还是外面,看到的都是并发执行的结果,原因是 Swoole 客户端的事件循环是统一的。而且 Swoole 的事件循环一旦开始,如果不手动执行 swoole_event_exit
,事件循环不会结束,你也就不会看到程序退出。在使用 Swoole 客户端时,需要注意这一点。
协程的参数和返回值
co
函数允许传参给协程。
而且 co
函数本身的返回值也是一个 promise
对象。它的值在 PHP 5 和 PHP 7 中略有差别。
PHP 5 的生成器函数本身不允许使用 return
返回值。例如:
- function test() {
- $x = (yield 1);
- return $x;
- }
这样的代码是非法的(但你不用担心因为写了这样的代码而被抓去坐牢">)。但是在 PHP 7 中,这种写法是被允许的,但这个返回值跟普通函数的返回值是不同的。PHP 7 中为 Generator
对象提供了一个 getReturn
方法来专门获取这个返回值。
co
函数的返回值在 PHP 5 中是最后一次执行的 yield 的返回值的 promise
包装。在 PHP 7 中,如果没有 return
语句,或者 return
语句没有返回值(或者返回值为 NULL
),那么,co
函数的返回值跟 PHP 5 相同。如果在 PHP 7 中,使用了 return
语句并且有返回值,比如像上面那个 test
函数,那么返回值为 return
语句返回值的 promise
包装。
因此如果你的代码是按照 PHP 5 的方式编写的,那么执行的效果在 PHP 5 和 PHP 7 中是相同的,如果你是按照 PHP 7 的方式单独编写的,那么你也能够得到你希望得到的 PHP 7 的返回值。因此,co
函数既做到了对旧版本的兼容性,又做到了对新版本特殊功能的支持。
因为 co
函数的结果本身也是一个 promise
对象,因此,你也可以在另外一个协程中来 yield
co
函数的执行结果。
下面这个例子演示了传参和 co
函数返回值的使用:
- use \Hprose\Future;
- use \Hprose\Http\Client;
- $test = new Client("http://hprose.com/example/");
- function hello($n, $test) {
- $result = array();
- for ($i = 0; $i < 5; $i++) {
- $result[] = $test->hello("$n-$i");
- }
- yield Future\all($result);
- }
- Future\co(function() use ($test) {
- $result = (yield Future\co(function($test) {
- $result = array();
- for ($i = 0; $i < 3; $i++) {
- $result[] = Future\co('hello', $i, $test);
- }
- yield Future\all($result);
- }, $test));
- var_dump($result);
- });
该程序执行结果为:
array(3) {
[0]=>
array(5) {
[0]=>
string(9) "Hello 0-0"
[1]=>
string(9) "Hello 0-1"
[2]=>
string(9) "Hello 0-2"
[3]=>
string(9) "Hello 0-3"
[4]=>
string(9) "Hello 0-4"
}
[1]=>
array(5) {
[0]=>
string(9) "Hello 1-0"
[1]=>
string(9) "Hello 1-1"
[2]=>
string(9) "Hello 1-2"
[3]=>
string(9) "Hello 1-3"
[4]=>
string(9) "Hello 1-4"
}
[2]=>
array(5) {
[0]=>
string(9) "Hello 2-0"
[1]=>
string(9) "Hello 2-1"
[2]=>
string(9) "Hello 2-2"
[3]=>
string(9) "Hello 2-3"
[4]=>
string(9) "Hello 2-4"
}
}
在这个程序里,所有的调用都是并发执行的,最后一次 yield
汇集最终所有结果。yield
语句在这里同时扮演了 return
的角色。
wrap 包装函数和 yield 的区别
我们在 Promise 异步编程 一章中,介绍了功能强大的 wrap
函数。通过它包装的函数可以直接将 promise
对象像普通参数一样带入函数执行。但是要注意,wrap
包装之后的函数虽然看上去像是同步的,但是实际上是异步执行的。当你有多个 wrap
包装的函数顺序执行的时候,实际上并不保证执行顺序按照书写顺序来。而 yield
则是同步的,它一定会保证 yield
语句的执行顺序。
我们来看一个例子:
- use \Hprose\Future;
- use \Hprose\Http\Client;
- $test = new Client("http://hprose.com/example/");
- Future\co(function() use ($test) {
- for ($i = 0; $i < 5; $i++) {
- var_dump((yield $test->hello("1-" . $i)));
- }
- $var_dump = Future\wrap('var_dump');
- for ($i = 0; $i < 5; $i++) {
- $var_dump($test->hello("2-" . $i));
- }
- for ($i = 0; $i < 5; $i++) {
- var_dump((yield $test->hello("3-" . $i)));
- }
- });
运行该程序之后,执行结果为:
string(9) "Hello 1-0"
string(9) "Hello 1-1"
string(9) "Hello 1-2"
string(9) "Hello 1-3"
string(9) "Hello 1-4"
string(9) "Hello 2-0"
string(9) "Hello 2-2"
string(9) "Hello 3-0"
string(9) "Hello 2-1"
string(9) "Hello 2-3"
string(9) "Hello 2-4"
string(9) "Hello 3-1"
string(9) "Hello 3-2"
string(9) "Hello 3-3"
string(9) "Hello 3-4"
这个结果可能每次执行都不一样。
但是,Hello 1-X
始终都是按照顺序输出的,而且始终都是在 Hello 2-Y
和 Hello 3-Z
之前输出的。
Hello 2-Y
的输出则不是按照顺序输出的(虽然偶尔结果也是按照顺序输出,但这一点并不能保证),而且它甚至还会穿插在 Hello 3-Z
的输出结果中。
Hello 3-Z
本身也是按照顺序输出的,但是 Hello 2-Y
却可能穿插在它的输出中间,原因是 Hello 2-Y
先执行,并且是异步执行的,因此它并不等结果执行完,就开始执行后面的语句了,所以当它执行完时,可能已经执行过几条 Hello 3-Z
的 yield
语句了。
将协程包装成闭包函数
wrap
函数不仅仅可以将普通函数包装成支持 promise
参数的函数。
wrap
函数还支持将协程包装成闭包函数的功能,包装之后的函数,不仅可以将协程当做普通函数一样执行,而且还支持传递 promise
参数。例如:
- use \Hprose\Future;
- use \Hprose\Http\Client;
- $test = new Client("http://hprose.com/example/");
- $coroutine = Future\wrap(function($test) {
- var_dump(1);
- var_dump((yield $test->hello("hprose")));
- $a = $test->sum(1, 2, 3);
- $b = $test->sum(4, 5, 6);
- $c = $test->sum(7, 8, 9);
- var_dump((yield $test->sum($a, $b, $c)));
- var_dump((yield $test->hello("world")));
- });
- $coroutine($test);
- $coroutine(Future\value($test));
该程序执行结果为:
int(1)
int(1)
string(12) "Hello hprose"
string(12) "Hello hprose"
int(45)
int(45)
string(11) "Hello world"
string(11) "Hello world"
我们会发现通过 wrap
函数包装的协程,不再需要使用 co
函数来执行了。
另外,wrap
函数包装的对象上的生成器方法也会自动变为普通方法。例如:
- use \Hprose\Future;
- class Test {
- function testco($x) {
- yield $x;
- }
- }
- $test = Future\wrap(new Test());
- $test->testco(123)->then('var_dump');
- $test->testco(Future\value('hello'))->then('var_dump');
该程序运行结果为:
int(123)
string(5) "hello"
协程与异常处理
在协程内,yield
不但可以将异步的 promise
结果转换成同步结果,而且可以将 REJECTED
状态的 promise
对象转换为抛出异常。例如:
- use Hprose\Client;
- use Hprose\Future;
- Future\co(function() {
- $client = Client::create('http://hprose.com/example/');
- try {
- (yield $client->ooxx());
- }
- catch (Exception $e) {
- echo $e->getMessage();
- }
- });
该程序运行结果为:
Can't find this function ooxx().
在协程内抛出的异常如果没有用 try
catch
语句捕获,那么第一个抛出的异常将会中断协程的执行,并将整个协程的返回值设置为 REJECTED
状态的 promise
对象,异常本身作为 reason
的值。例如:
- use Hprose\Client;
- use Hprose\Future;
- Future\co(function() {
- $client = Client::create('http://hprose.com/example/');
- (yield $client->oo());
- (yield $client->xx());
- })->catchError(function($e) {
- echo $e->getMessage();
- });
该程序运行结果为:
Can't find this function oo().
原文: https://github.com/hprose/hprose-php/wiki/04-%E5%8D%8F%E7%A8%8B