call/cc

wiki - call-with-current-continuation

the function call-with-current-continuation, abbreviated call/cc, is a control operator

异步回调API是无法直接使用yield语法的,需要使用thunk或者promise进行转换,thunkify是将多参数函数替换成单参数函数且参数只接受回调(参见:Thunk 函数的含义和用法)。上文中我们将回调api显式实现Async接口,显得有些麻烦,这里可以把 “通过call参数$k传递异步结果” 的模式抽象出来,实现一个穷人的call/cc。

  1. <?php
  2. // CallCC instanceof Async
  3. class CallCC implements Async
  4. {
  5. public $fun;
  6. public function __construct(callable $fun)
  7. {
  8. $this->fun = $fun;
  9. }
  10. public function begin(callable $continuation)
  11. {
  12. $fun = $this->fun;
  13. $fun($continuation);
  14. }
  15. }
  16. function callcc(callable $fn)
  17. {
  18. return new CallCC($fn);
  19. }

wiki - call-with-current-continuation

Taking a function f as its only argument, call/cc takes the current continuation as an object and applies f to it. The continuation object is a first-class value and is represented as a function, with function application as its only operation. When a continuation object is applied to an argument, the existing continuation is eliminated and the applied continuation is restored in its place, so that the program flow will continue at the point at which the continuation was captured and the argument of the continuation then becomes the “return value” of the call/cc invocation. Continuations created with call/cc may be called more than once, and even from outside the dynamic extent of the call/cc application.

我们的call/cc只可以调用一次(Generator是单向的),虽然我们的$k也是first-class value,但即使$k($k)进行传递,也无法达到wiki介绍的效果,PHP不支持Continuation,我们创造的半协程中的call/cc的功能有限,仅仅借用了call/cc的形式。

事实上,yield只能将控制权从Generator转移到起caller中:

Wiki - Coroutine

Generators, also known as semicoroutines, are also a generalisation of subroutines, but are more limited than coroutines. Specifically, while both of these can yield multiple times, suspending their execution and allowing re-entry at multiple entry points, they differ in that coroutines can control where execution continues after they yield, while generators cannot, instead transferring control back to the generator’s caller. That is, since generators are primarily used to simplify the writing of iterators, the yield statement in a generator does not specify a coroutine to jump to, but rather passes a value back to a parent routine.

However, it is still possible to implement coroutines on top of a generator facility, with the aid of a top-level dispatcher routine (a trampoline, essentially) that passes control explicitly to child generators identified by tokens passed back from the generators

来看例子:

  1. <?php
  2. function async_sleep($ms)
  3. {
  4. return callcc(function($k) use($ms) {
  5. swoole_timer_after($ms, function() use($k) {
  6. $k(null);
  7. });
  8. });
  9. }
  10. function async_dns_lookup($host)
  11. {
  12. return callcc(function($k) use($host) {
  13. swoole_async_dns_lookup($host, function($host, $ip) use($k) {
  14. $k($ip);
  15. });
  16. });
  17. }
  18. class HttpClient extends \swoole_http_client
  19. {
  20. public function async_get($uri)
  21. {
  22. return callcc(function($k) use($uri) {
  23. $this->get($uri, $k);
  24. });
  25. }
  26. public function async_post($uri, $post)
  27. {
  28. return callcc(function($k) use($uri, $post) {
  29. $this->post($uri, $post, $k);
  30. });
  31. }
  32. public function async_execute($uri)
  33. {
  34. return callcc(function($k) use($uri) {
  35. $this->execute($uri, $k);
  36. });
  37. }
  38. }
  39. // 这里!
  40. spawn(function() {
  41. $ip = (yield async_dns_lookup("www.baidu.com"));
  42. $cli = new HttpClient($ip, 80);
  43. $cli->setHeaders(["foo" => "bar"]);
  44. $cli = (yield $cli->async_get("/"));
  45. echo $cli->body, "\n";
  46. });

我们可以用相同的方式来封装swoole其他的异步api(TcpClient,MysqlClient,RedisClient…),大家可以举一反三(建议继承swoole原生类,而不是直接实现Async)。