├── .gitignore ├── README.md ├── SUMMARY.md ├── can-kao.md ├── di-er-bu-52063a-koa.md ├── di-er-bu-52063a-koa ├── chuan-yue-di-xin-zhi-lv.md ├── koa-helloworld.md ├── koa-qian-yan.md ├── koaapplication.md ├── koacontext.md ├── koarequest.md ├── koaresponse.md ├── middleware-interface.md ├── middleware-qing-qiu-chao-shi.md ├── middleware-quan-ju-yi-chang-chu-li.md ├── middleware-router.md ├── rightreduceyu-zhong-jian-jian-compose.md ├── yang-cong-quan-mo-xing.md └── yi-ge-zong-he-shi-li.md ├── di-yi-bu-52063a-ban-xie-cheng-diao-du-qi.md ├── di-yi-bu-52063a-ban-xie-cheng-diao-du-qi ├── allyu-parallel.md ├── callcc.md ├── channelyan-shi.md ├── channelyu-xie-cheng-jian-tong-xin.md ├── chou-xiang-yi-bu-mo-xing.md ├── diao-du-56683a-li-cheng-bei.md ├── futuretaskyu-fork.md ├── huan-cun-channel.md ├── qi-ta-you-qu-de-zu-jian.md ├── raceyu-timeout.md ├── returngai-xie.md ├── sheng-cheng-qi-die-dai.md ├── sheng-cheng-qi-fan-hui-zhi.md ├── sheng-cheng-qi-wei-tuo.md ├── sheng-cheng-qi.md ├── spawn.md ├── syscallyu-context.md ├── wu-huancun-channel.md ├── yi-5e383a-chuan-di-liu-cheng.md ├── yi-5e383a-qian-tao-ren-wu-tou-chuan.md ├── yi-5e383a-zhong-xin-jin-xing-cps-bian-huan.md ├── yi-5e383azhong-xin-jia-ru-async.md └── yin-ru-yi-chang-chu-li.md ├── fu-lu.md ├── php-co-koa.pdf ├── qian-yan.md ├── qian-yan └── shuo-ming.md └── shuo-ming.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PHP异步编程: 手把手教你实现co与Koa 2 | 3 | * [前言](qian-yan.md) 4 | * [说明](qian-yan/shuo-ming.md) 5 | * [第一部分: 半协程调度器](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi.md) 6 | * [统一生成器接口](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi.md) 7 | * [生成器迭代](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-die-dai.md) 8 | * [生成器返回值](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-fan-hui-zhi.md) 9 | * [生成器委托](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-wei-tuo.md) 10 | * [改写return](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/returngai-xie.md) 11 | * [抽象异步模型](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/chou-xiang-yi-bu-mo-xing.md) 12 | * [引入异常处理](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yin-ru-yi-chang-chu-li.md) 13 | * [异常: 嵌套任务透传](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-qian-tao-ren-wu-tou-chuan.md) 14 | * [异常: 传递流程](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-chuan-di-liu-cheng.md) 15 | * [异常: 重新进行CPS变换](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-zhong-xin-jin-xing-cps-bian-huan.md) 16 | * [异常: 重新加入Async](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383azhong-xin-jia-ru-async.md) 17 | * [Syscall与Context](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/syscallyu-context.md) 18 | * [调度器: 里程碑](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/diao-du-56683a-li-cheng-bei.md) 19 | * [spawn](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/spawn.md) 20 | * [callcc](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/callcc.md) 21 | * [race与timeout](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/raceyu-timeout.md) 22 | * [all与parallel](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/allyu-parallel.md) 23 | * [channel与协程间通信](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/channelyu-xie-cheng-jian-tong-xin.md) 24 | * [无缓存channel](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/wu-huancun-channel.md) 25 | * [缓存channel](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/huan-cun-channel.md) 26 | * [channel演示](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/channelyan-shi.md) 27 | * [FutureTask与fork](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/futuretaskyu-fork.md) 28 | * [第二部分: Koa](di-er-bu-52063a-koa.md) 29 | * [穿越地心之旅](di-er-bu-52063a-koa/chuan-yue-di-xin-zhi-lv.md) 30 | * [洋葱圈模型](di-er-bu-52063a-koa/yang-cong-quan-mo-xing.md) 31 | * [rightReduce与中间件compose](di-er-bu-52063a-koa/rightreduceyu-zhong-jian-jian-compose.md) 32 | * [Koa::Application](di-er-bu-52063a-koa/koaapplication.md) 33 | * [Koa::Context](di-er-bu-52063a-koa/koacontext.md) 34 | * [Koa::Request](di-er-bu-52063a-koa/koarequest.md) 35 | * [Koa::Response](di-er-bu-52063a-koa/koaresponse.md) 36 | * [Koa - HelloWorld](di-er-bu-52063a-koa/koa-helloworld.md) 37 | * [Middleware Interface](di-er-bu-52063a-koa/middleware-interface.md) 38 | * [Middleware: 全局异常处理](di-er-bu-52063a-koa/middleware-quan-ju-yi-chang-chu-li.md) 39 | * [Middleware: Router](di-er-bu-52063a-koa/middleware-router.md) 40 | * [Middleware: 请求超时](di-er-bu-52063a-koa/middleware-qing-qiu-chao-shi.md) 41 | * [一个综合示例](di-er-bu-52063a-koa/yi-ge-zong-he-shi-li.md) 42 | * [附录](fu-lu.md) 43 | * [参考](can-kao.md) 44 | 45 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [简介](README.md) 4 | * [前言](qian-yan.md) 5 | * [说明](qian-yan/shuo-ming.md) 6 | * [第一部分: 半协程调度器](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi.md) 7 | * [统一生成器接口](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi.md) 8 | * [生成器迭代](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-die-dai.md) 9 | * [生成器返回值](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-fan-hui-zhi.md) 10 | * [生成器委托](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-wei-tuo.md) 11 | * [改写return](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/returngai-xie.md) 12 | * [抽象异步模型](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/chou-xiang-yi-bu-mo-xing.md) 13 | * [引入异常处理](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yin-ru-yi-chang-chu-li.md) 14 | * [异常: 嵌套任务透传](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-qian-tao-ren-wu-tou-chuan.md) 15 | * [异常: 传递流程](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-chuan-di-liu-cheng.md) 16 | * [异常: 重新进行CPS变换](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-zhong-xin-jin-xing-cps-bian-huan.md) 17 | * [异常: 重新加入Async](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383azhong-xin-jia-ru-async.md) 18 | * [Syscall与Context](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/syscallyu-context.md) 19 | * [调度器: 里程碑](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/diao-du-56683a-li-cheng-bei.md) 20 | * [spawn](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/spawn.md) 21 | * [callcc](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/callcc.md) 22 | * [race与timeout](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/raceyu-timeout.md) 23 | * [all与parallel](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/allyu-parallel.md) 24 | * [channel与协程间通信](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/channelyu-xie-cheng-jian-tong-xin.md) 25 | * [无缓存channel](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/wu-huancun-channel.md) 26 | * [缓存channel](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/huan-cun-channel.md) 27 | * [channel演示](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/channelyan-shi.md) 28 | * [FutureTask与fork](di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/futuretaskyu-fork.md) 29 | * [第二部分: Koa](di-er-bu-52063a-koa.md) 30 | * [穿越地心之旅](di-er-bu-52063a-koa/chuan-yue-di-xin-zhi-lv.md) 31 | * [洋葱圈模型](di-er-bu-52063a-koa/yang-cong-quan-mo-xing.md) 32 | * [rightReduce与中间件compose](di-er-bu-52063a-koa/rightreduceyu-zhong-jian-jian-compose.md) 33 | * [Koa::Application](di-er-bu-52063a-koa/koaapplication.md) 34 | * [Koa::Context](di-er-bu-52063a-koa/koacontext.md) 35 | * [Koa::Request](di-er-bu-52063a-koa/koarequest.md) 36 | * [Koa::Response](di-er-bu-52063a-koa/koaresponse.md) 37 | * [Koa - HelloWorld](di-er-bu-52063a-koa/koa-helloworld.md) 38 | * [Middleware Interface](di-er-bu-52063a-koa/middleware-interface.md) 39 | * [Middleware: 全局异常处理](di-er-bu-52063a-koa/middleware-quan-ju-yi-chang-chu-li.md) 40 | * [Middleware: Router](di-er-bu-52063a-koa/middleware-router.md) 41 | * [Middleware: 请求超时](di-er-bu-52063a-koa/middleware-qing-qiu-chao-shi.md) 42 | * [一个综合示例](di-er-bu-52063a-koa/yi-ge-zong-he-shi-li.md) 43 | * [附录](fu-lu.md) 44 | * [参考](can-kao.md) 45 | 46 | -------------------------------------------------------------------------------- /can-kao.md: -------------------------------------------------------------------------------- 1 | # 参考 2 | 3 | 1. [koa - Github](https://github.com/koajs/koa) 4 | 2. [koa - 官网](http://koajs.com/) 5 | 3. [Koa - 中文文档](https://github.com/guo-yu/Koa-guide) 6 | 4. [wiki - Coroutine](https://en.wikipedia.org/wiki/Coroutine) 7 | 5. [wiki - 地球构造](https://zh.wikipedia.org/wiki/%E5%9C%B0%E7%90%83%E6%A7%8B%E9%80%A0) 8 | 6. [nikic - 在PHP中使用协程实现多任务调度(译文)](http://www.laruence.com/2015/05/28/3038.html) 9 | 7. [nikic - FastRoute](https://github.com/nikic/FastRoute) 10 | 8. [阮一峰 - Generator 函数的含义与用法](http://www.ruanyifeng.com/blog/2015/04/generator.html) 11 | 9. [阮一峰 - Thunk 函数的含义和用法](http://www.ruanyifeng.com/blog/2015/05/thunk.html) 12 | 10. [阮一峰 - co 函数库的含义和用法](http://www.ruanyifeng.com/blog/2015/05/co.html) 13 | 11. [PHP RFC - generator](https://wiki.php.net/rfc/generators) 14 | 12. [PHP RFC - delegating generator](https://wiki.php.net/rfc/generator-delegation) -------------------------------------------------------------------------------- /di-er-bu-52063a-koa.md: -------------------------------------------------------------------------------- 1 | # 第二部分: Koa 2 | 3 | Koa自述是下一代web框架: 4 | 5 | > 由 Express 原班人马打造的 Koa,致力于成为一个更小、更健壮、更富有表现力的 Web 框架。 6 | > 使用 Koa 编写 web 应用,通过组合不同的 generator, 7 | > 可以免除重复繁琐的回调函数嵌套,并极大地提升常用错误处理效率。 8 | > Koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。 9 | 10 | 像`Ruby Rack` `Python django` `Golang matini` `Node Express` `PHP Laravel` `Java Spring`,Web框架大多会有一个面向AOP的中间件模块,内部操纵Req/Res对象可选执行next动作,Koa与martini类似,都属于设计清爽的中间件web框架,采用洋葱模型middleware stack,但合并了Request与Response对象,编写更直观方便。 11 | 12 | > Koa's middleware stack flows in a stack-like manner, 13 | > allowing you to perform actions downstream then filter and manipulate the response upstream. 14 | > Each middleware receives a Koa `Context` object that encapsulates an incoming 15 | > http message and the corresponding response to that message. `ctx` is often used 16 | > as the parameter name for the context object. 17 | 18 | [Laravel Middleware](https://laravel.com/docs/5.4/middleware) 19 | Laravel after方法的书写不够自然,需要中间变量保存Response; Spring的Interceptor功能强大,但编写略繁琐; Koa则简单的多,而且可以在某个中间件中灵活控制之后中间件执行时机。 20 | 21 | 其实对于web框架来讲,业务逻辑归根结底都在处理请求与相应对象,web中间件实质就是在请求与响应中间开放出来的可编排的扩展点,比如修改请求做URLRewrite,比如请求日志,身份验证... 22 | 23 | 真正的业务逻辑都可以通过middleware实现,或者说按特定顺序对中间件灵活组合编排。 24 | 25 | 我们基于PHP5.6与yz-swoole(有赞内部自研稳定版本的swoole,17年6月即将开源,敬请期待),用少量的代码即可实(chao)现(xi)一个php-koa。 26 | 27 | Koa2.x决定全面使用async/await,受限于PHP语法,我们这里仅实现Koa1.x,将Generator作为middleware实现形式。 -------------------------------------------------------------------------------- /di-er-bu-52063a-koa/chuan-yue-di-xin-zhi-lv.md: -------------------------------------------------------------------------------- 1 | ## 穿越地心之旅 2 | 3 | (如果你了解洋葱圈模型,略过本小节) 4 | 5 | 首先让我们对洋葱圈模型有一个直观的认识: 6 | 7 | [地球构造](https://zh.wikipedia.org/wiki/%E5%9C%B0%E7%90%83%E6%A7%8B%E9%80%A0) 8 | 9 | > 物理学上,地球可划分为岩石圈、软流层、地幔、外核和内核5层。 10 | 11 | > 化学上,地球被划分为地壳、上地幔、下地幔、外核和内核。地质学上对地球各层的划分 12 | 13 | 演示Koa的中间件之前,我们用函数来描述一场穿越地心之旅: 14 | 15 | ------------------------------------------------------ 16 | 17 | ```php 18 | \n"; 22 | $next(); 23 | echo "离开<地壳>\n"; 24 | } 25 | 26 | function upperMantle($next) 27 | { 28 | echo "到达<上地幔>\n"; 29 | $next(); 30 | echo "离开<上地幔>\n"; 31 | } 32 | 33 | function mantle($next) 34 | { 35 | echo "到达<下地幔>\n"; 36 | $next(); 37 | echo "离开<下地幔>\n"; 38 | } 39 | 40 | function outerCore($next) 41 | { 42 | echo "到达<外核>\n"; 43 | $next(); 44 | echo "离开<外核>\n"; 45 | } 46 | 47 | function innerCore($next) 48 | { 49 | echo "到达<内核>\n"; 50 | } 51 | 52 | // 我们逆序组合组合, 返回入口 53 | function makeTravel(...$layers) 54 | { 55 | $next = null; 56 | $i = count($layers); 57 | while ($i--) { 58 | $layer = $layers[$i]; 59 | $next = function() use($layer, $next) { 60 | // 这里next指向穿越下一次的函数,作为参数传递给上一层调用 61 | $layer($next); 62 | }; 63 | } 64 | return $next; 65 | } 66 | 67 | 68 | // 我们途径 crust -> upperMantle -> mantle -> outerCore -> innerCore 到达地心 69 | // 然后穿越另一半球 -> outerCore -> mantle -> upperMantle -> crust 70 | 71 | $travel = makeTravel("crust", "upperMantle", "mantle", "outerCore", "innerCore"); 72 | $travel(); // output: 73 | /* 74 | 到达<地壳> 75 | 到达<上地幔> 76 | 到达<下地幔> 77 | 到达<外核> 78 | 到达<内核> 79 | 离开<外核> 80 | 离开<下地幔> 81 | 离开<上地幔> 82 | 离开<地壳> 83 | */ 84 | ``` 85 | 86 | ------------------------------------------------------ 87 | 88 | ```php 89 | \n"; 94 | // 我们放弃内核,仅仅绕外壳一周,从另一侧返回地表 95 | // $next(); 96 | echo "离开<外核>\n"; 97 | } 98 | 99 | 100 | $travel = makeTravel("crust", "upperMantle", "mantle", "outerCore1", "innerCore"); 101 | $travel(); // output: 102 | /* 103 | 到达<地壳> 104 | 到达<上地幔> 105 | 到达<下地幔> 106 | 到达<外核> 107 | 离开<外核> 108 | 离开<下地幔> 109 | 离开<上地幔> 110 | 离开<地壳> 111 | */ 112 | ``` 113 | 114 | ------------------------------------------------------ 115 | 116 | ```php 117 | \n"; 123 | } 124 | 125 | 126 | $travel = makeTravel("crust", "upperMantle", "mantle", "outerCore", "innerCore1"); 127 | $travel(); // output: 128 | /* 129 | 到达<地壳> 130 | 到达<上地幔> 131 | 到达<下地幔> 132 | 到达<外核> 133 | Fatal error: Uncaught exception 'Exception' with message '岩浆' 134 | */ 135 | ``` 136 | 137 | ------------------------------------------------------ 138 | 139 | ```php 140 | \n"; 145 | // 我们在下地幔的救援团队及时赶到 (try catch) 146 | try { 147 | $next(); 148 | } catch (\Exception $ex) { 149 | echo "遇到", $ex->getMessage(), "\n"; 150 | } 151 | // 我们仍旧没有去往内核,,绕道对端下地幔,返回地表 152 | echo "离开<下地幔>\n"; 153 | } 154 | 155 | 156 | $travel = makeTravel("crust", "upperMantle", "mantle1", "outerCore", "innerCore1"); 157 | $travel(); // output: 158 | /* 159 | 到达<地壳> 160 | 到达<上地幔> 161 | 到达<下地幔> 162 | 到达<外核> 163 | 遇到岩浆 164 | 离开<下地幔> 165 | 离开<上地幔> 166 | 离开<地壳> 167 | */ 168 | ``` 169 | 170 | ------------------------------------------------------ 171 | 172 | ```php 173 | \n"; 179 | $next(); 180 | // 只在返程时暂留 181 | echo "离开<上地幔>\n"; 182 | } 183 | 184 | function outerCore2($next) 185 | { 186 | // 我们决定只在去程考察外核 187 | echo "到达<外核>\n"; 188 | $next(); 189 | // 因为温度过高,去程匆匆离开外壳 190 | // echo "离开<外核>\n"; 191 | } 192 | 193 | 194 | $travel = makeTravel("crust", "upperMantle1", "mantle1", "outerCore2", "innerCore1"); 195 | $travel(); // output: 196 | /* 197 | 到达<地壳> 198 | 到达<上地幔> 199 | 到达<下地幔> 200 | 到达<外核> 201 | 遇到岩浆 202 | 离开<下地幔> 203 | 离开<上地幔> 204 | 离开<地壳> 205 | */ 206 | ``` 207 | -------------------------------------------------------------------------------- /di-er-bu-52063a-koa/koa-helloworld.md: -------------------------------------------------------------------------------- 1 | ## Koa - HelloWorld 2 | 3 | 以上便是全部了,我们重点来看示例,我们只注册一个中间件, Hello Worler Server: 4 | 5 | ```php 6 | υse(function(Context $ctx) { 13 | $ctx->status = 200; 14 | $ctx->body = "
" . print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true); 32 | }); 33 | 34 | $router->get('/timeout', function(Context $ctx) { 35 | // 超时 36 | yield async_sleep(500); 37 | }); 38 | 39 | $router->get('/error/http_exception', function(Context $ctx) { 40 | // 抛出带status的错误 41 | $ctx->thrοw(500, "Internal Error"); 42 | // 等价于 throw new HttpException(500, "Internal Error"); 43 | yield; 44 | }); 45 | 46 | $router->get('/error/exception', function(Context $ctx) { 47 | // 直接抛出错误, 500 错误 48 | throw new \Exception("some internal error", 10000); 49 | yield; 50 | }); 51 | 52 | $router->get('/user/{id:\d+}', function(Context $ctx, $next, $vars) { 53 | $ctx->body = "user={$vars['id']}"; 54 | yield; 55 | }); 56 | 57 | // http://127.0.0.1:3000/request/www.baidu.com 58 | $router->get('/request/{url}', function(Context $ctx, $next, $vars) { 59 | $r = (yield async_curl_get($vars['url'])); 60 | $ctx->body = $r->body; 61 | $ctx->status = $r->statusCode; 62 | }); 63 | 64 | $app->υse($router->routes()); 65 | 66 | 67 | $app->υse(function(Context $ctx) { 68 | $ctx->status = 200; 69 | $ctx->body = "Hello World
"; 70 | yield; 71 | }); 72 | 73 | $app->listen(3000); 74 | 75 | ``` 76 | 77 | 以上我们完成了一个基于swoole的版本的php-koa。 -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi.md: -------------------------------------------------------------------------------- 1 | # 第一部分: 半协程调度器 2 | 3 | 谈及koa(1.x)首先得说co,co与Promise是JSer在解决回调地狱(callback hell)问题前仆后继的众多产物之一。 4 | 5 | co其实是Generator的自动执行器(半协程调度器): 通过yield显式操纵控制流让我们可以做到以近乎同步的方式书写非阻塞代码。 6 | 7 | Promise是一套比较完善的方案,但关于如何实现Promise本身超出本文范畴, 且PHP没有大量异步接口的历史包袱需要thunks方案做转换。 8 | 9 | 综上所述, 我们的调度器仅基于一个简单的接口,来抽象异步任务: 10 | 11 | ```php 12 | tasks = $tasks; 21 | $this->parent = $parent; 22 | $this->n = count($tasks); 23 | assert($this->n > 0); 24 | $this->results = []; 25 | } 26 | 27 | public function begin(callable $continuation = null) 28 | { 29 | $this->continuation = $continuation; 30 | foreach ($this->tasks as $id => $task) { 31 | (new AsyncTask($task, $this->parent))->begin($this->continuation($id)); 32 | }; 33 | } 34 | 35 | private function continuation($id) 36 | { 37 | return function($r, $ex = null) use($id) { 38 | if ($this->done) { 39 | return; 40 | } 41 | 42 | // 任一回调发生异常,终止任务 43 | if ($ex) { 44 | $this->done = true; 45 | $k = $this->continuation; 46 | $k(null, $ex); 47 | return; 48 | } 49 | 50 | $this->results[$id] = $r; 51 | if (--$this->n === 0) { 52 | // 所有回调完成,终止任务 53 | $this->done = true; 54 | if ($this->continuation) { 55 | $k = $this->continuation; 56 | $k($this->results); 57 | } 58 | } 59 | }; 60 | } 61 | } 62 | 63 | 64 | function all(array $tasks) 65 | { 66 | $tasks = array_map(__NAMESPACE__ . "\\await", $tasks); 67 | 68 | return new Syscall(function(AsyncTask $parent) use($tasks) { 69 | if (empty($tasks)) { 70 | return null; 71 | } else { 72 | return new All($tasks, $parent); 73 | } 74 | }); 75 | } 76 | ``` 77 | 78 | ```php 79 | 93 | string(14) "202.89.233.103" 94 | [1]=> 95 | string(14) "125.88.193.243" 96 | [2]=> 97 | string(15) "115.239.211.112" 98 | } 99 | */ 100 | } catch (\Exception $ex) { 101 | echo $ex; 102 | } 103 | }); 104 | ``` 105 | 106 | 107 | 我们这里实现了与Promise.all相同语义的接口,或者更复杂一些,我们也可以实现批量任务以chunk方式进行作业的接口,留待读者自己完成; 108 | 109 | 至此, 我们已经拥有了 `spawn` `callcc` `race` `all` `timeout`. -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/callcc.md: -------------------------------------------------------------------------------- 1 | ## call/cc 2 | 3 | [wiki - call-with-current-continuation](https://en.wikipedia.org/wiki/Call-with-current-continuation) 4 | 5 | > the function call-with-current-continuation, abbreviated call/cc, is a control operator 6 | 7 | 异步回调API是无法直接使用yield语法的,需要使用thunk或者promise进行转换,thunkify是将多参数函数替换成单参数函数且参数只接受回调(参见:[Thunk 函数的含义和用法](http://www.ruanyifeng.com/blog/2015/05/thunk.html))。上文中我们将回调api显式实现Async接口,显得有些麻烦,这里可以把 **“通过call参数$k传递异步结果”** 的模式抽象出来,实现一个穷人的call/cc。 8 | 9 | ```php 10 | fun = $fun; 20 | } 21 | 22 | public function begin(callable $continuation) 23 | { 24 | $fun = $this->fun; 25 | $fun($continuation); 26 | } 27 | } 28 | 29 | function callcc(callable $fn) 30 | { 31 | return new CallCC($fn); 32 | } 33 | ``` 34 | 35 | [wiki - call-with-current-continuation](https://en.wikipedia.org/wiki/Call-with-current-continuation) 36 | 37 | > 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. 38 | 39 | 我们的call/cc只可以调用一次(Generator是单向的),虽然我们的$k也是`first-class value`,但即使`$k($k)`进行传递,也无法达到wiki介绍的效果,PHP不支持Continuation,我们创造的半协程中的call/cc的功能有限,仅仅借用了call/cc的形式。 40 | 41 | 事实上,yield只能将控制权从Generator转移到起caller中: 42 | 43 | [Wiki - Coroutine](https://en.wikipedia.org/wiki/Coroutine) 44 | 45 | > 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. 46 | 47 | > 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 48 | 49 | 50 | 来看例子: 51 | 52 | 53 | ```php 54 | get($uri, $k); 80 | }); 81 | } 82 | 83 | public function async_post($uri, $post) 84 | { 85 | return callcc(function($k) use($uri, $post) { 86 | $this->post($uri, $post, $k); 87 | }); 88 | } 89 | 90 | public function async_execute($uri) 91 | { 92 | return callcc(function($k) use($uri) { 93 | $this->execute($uri, $k); 94 | }); 95 | } 96 | } 97 | 98 | // 这里! 99 | spawn(function() { 100 | $ip = (yield async_dns_lookup("www.baidu.com")); 101 | $cli = new HttpClient($ip, 80); 102 | $cli->setHeaders(["foo" => "bar"]); 103 | $cli = (yield $cli->async_get("/")); 104 | echo $cli->body, "\n"; 105 | }); 106 | 107 | ``` 108 | 109 | 我们可以用相同的方式来封装swoole其他的异步api(TcpClient,MysqlClient,RedisClient...),大家可以举一反三(建议继承swoole原生类,而不是直接实现Async)。 -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/channelyan-shi.md: -------------------------------------------------------------------------------- 1 | ## channel演示 2 | 3 | 这是我们最终得到的接口: 4 | 5 | 6 | ```php 7 | recv()); 41 | yield $pongCh->send("PONG\n"); 42 | 43 | // 递归调度器实现,需要引入异步的方法退栈,否则Stack Overflow... 44 | // 或者考虑将send或者recv以defer方式实现 45 | yield async_sleep(1); 46 | } 47 | }); 48 | 49 | go(function() use($pingCh, $pongCh) { 50 | while (true) { 51 | echo (yield $pongCh->recv()); 52 | yield $pingCh->send("PING\n"); 53 | 54 | yield async_sleep(1); 55 | } 56 | }); 57 | 58 | // start up 59 | go(function() use($pingCh) { 60 | echo "start up\n"; 61 | yield $pingCh->send("PING"); 62 | }); 63 | 64 | // output: 65 | /* 66 | start up 67 | PING 68 | PONG 69 | PING 70 | PONG 71 | PING 72 | PONG 73 | PING 74 | ... 75 | */ 76 | 77 | ``` 78 | 79 | 当然,我们可以很轻易构建一个生产者-消费者模型: 80 | 81 | 82 | ```php 83 | send("producer 1"); 91 | yield async_sleep(1000); 92 | } 93 | }); 94 | 95 | // 生产者2 96 | go(function() use($ch) { 97 | while (true) { 98 | yield $ch->send("producer 2"); 99 | yield async_sleep(1000); 100 | } 101 | }); 102 | 103 | // 消费者1 104 | go(function() use($ch) { 105 | while (true) { 106 | $recv = (yield $ch->recv()); 107 | echo "consumer1: $recv\n"; 108 | } 109 | }); 110 | 111 | // 消费者2 112 | go(function() use($ch) { 113 | while (true) { 114 | $recv = (yield $ch->recv()); 115 | echo "consumer2: $recv\n"; 116 | } 117 | }); 118 | 119 | // output: 120 | /* 121 | consumer1 recv from producer 1 122 | consumer1 recv from producer 2 123 | consumer1 recv from producer 1 124 | consumer2 recv from producer 2 125 | consumer1 recv from producer 2 126 | consumer2 recv from producer 1 127 | consumer1 recv from producer 1 128 | consumer2 recv from producer 2 129 | consumer1 recv from producer 2 130 | consumer2 recv from producer 1 131 | consumer1 recv from producer 1 132 | consumer2 recv from producer 2 133 | ...... 134 | */ 135 | 136 | ``` 137 | 138 | channel 自身是`first-class value`, 所以可传递: 139 | 140 | 141 | ```php 142 | send($anotherCh); 152 | echo "send another channel\n"; 153 | yield $anotherCh->send("HELLO"); 154 | echo "send hello through another channel\n"; 155 | }); 156 | 157 | go(function() use($ch) { 158 | /** @var Channel $anotherCh */ 159 | $anotherCh = (yield $ch->recv()); 160 | echo "recv another channel\n"; 161 | $val = (yield $anotherCh->recv()); 162 | echo $val, "\n"; 163 | }); 164 | 165 | // output: 166 | /* 167 | send another channel 168 | recv another channel 169 | send hello through another channel 170 | HELLO 171 | */ 172 | ``` 173 | 174 | 我们通过控制channel缓存大小 观察输出结果: 175 | 176 | ```php 177 | recv()); 182 | echo "recv $recv\n"; 183 | $recv = (yield $ch->recv()); 184 | echo "recv $recv\n"; 185 | $recv = (yield $ch->recv()); 186 | echo "recv $recv\n"; 187 | $recv = (yield $ch->recv()); 188 | echo "recv $recv\n"; 189 | }); 190 | 191 | go(function() use($ch) { 192 | yield $ch->send(1); 193 | echo "send 1\n"; 194 | yield $ch->send(2); 195 | echo "send 2\n"; 196 | yield $ch->send(3); 197 | echo "send 3\n"; 198 | yield $ch->send(4); 199 | echo "send 4\n"; 200 | }); 201 | 202 | // $n = 1; 203 | // output: 204 | /* 205 | send 1 206 | recv 1 207 | send 2 208 | recv 2 209 | send 3 210 | recv 3 211 | send 4 212 | recv 4 213 | */ 214 | 215 | // $n = 2; 216 | // output: 217 | /* 218 | send 1 219 | send 2 220 | recv 1 221 | recv 2 222 | send 3 223 | send 4 224 | recv 3 225 | recv 4 226 | */ 227 | 228 | // $n = 3; 229 | // output: 230 | /* 231 | send 1 232 | send 2 233 | send 3 234 | recv 1 235 | recv 2 236 | recv 3 237 | send 4 238 | recv 4 239 | */ 240 | 241 | ``` 242 | 243 | 244 | 一个更具体的生产者消费者的例子: 245 | 246 | 247 | ```php 248 | recv()); 258 | yield $file->write("$host: $status\n"); 259 | } 260 | }); 261 | 262 | // 请求并写入chan 263 | go(function() use($ch) { 264 | while (true) { 265 | $host = "www.baidu.com"; 266 | $resp = (yield async_curl_get($host)); 267 | yield $ch->send([$host, $resp->statusCode]); 268 | } 269 | }); 270 | 271 | // 请求并写入chan 272 | go(function() use($ch) { 273 | while (true) { 274 | $host = "www.bing.com"; 275 | $resp = (yield async_curl_get($host)); 276 | yield $ch->send([$host, $resp->statusCode]); 277 | } 278 | }); 279 | 280 | // output: 281 | ``` 282 | 283 | 284 | channel的发送与接受没有超时机制,Golang可以select多个chan实现超时处理,我们可以做一个select设施,或者在send于recv接受直接添加超时参数,扩展接口功能,留待读者自行实现。 -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/channelyu-xie-cheng-jian-tong-xin.md: -------------------------------------------------------------------------------- 1 | ## channel与协程间通信 2 | 3 | 虽然已经构建了基于yield的半协程,之前所有讨论都集中在单个协程,我们可以再深入一步,构造带有阻塞语义的协程间通信原语--channel,这里按照Golang的channel来实现; 4 | 5 | [playground](https://tour.golang.org/concurrency/2) 6 | 7 | > By default, sends and receives block until the other side is ready. 8 | 9 | > This allows goroutines to synchronize without explicit locks or condition variables. 10 | 11 | 相比Golang,我们因为只有一个线程,对于chan发送与接收的阻塞的处理,最终会被转换为对使用channel的协程的控制权的转移。 -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/chou-xiang-yi-bu-mo-xing.md: -------------------------------------------------------------------------------- 1 | ## 抽象异步模型 2 | 3 | 对回调模型抽象出异步接口`Async`。 4 | 5 | 只有一个方法的接口通常都可以使用闭包代替,区别在于interface引入新类型,闭包则不会。如果说thunkify依赖了参数顺序的弱约定,Async相对严肃的依赖了类型。 6 | 7 | ```php 8 | gen->send($result); 21 | 22 | if ($this->gen->valid()) { 23 | // \Generator -> Async 24 | if ($value instanceof \Generator) { 25 | $value = new self($value); 26 | } 27 | 28 | if ($value instanceof Async) { 29 | $async = $value; 30 | $continuation = [$this, "next"]; 31 | $async->begin($continuation); 32 | } else { 33 | $this->next($value); 34 | } 35 | 36 | } else { 37 | $cc = $this->continuation; 38 | $cc($result); 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | 两个简单的对回调接口转换例子: 45 | 46 | ```php 47 | begin($trace); 85 | // output: 86 | // 1 87 | // IP: 115.239.210.27 88 | 89 | ``` 90 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/diao-du-56683a-li-cheng-bei.md: -------------------------------------------------------------------------------- 1 | ## 调度器: 里程碑 2 | 3 | 除去接口声明不到60行代码,我们实现了支持`任务嵌套`与`异常处理`,并可以通过`Syscall`扩充功能的半协程调度器。 4 | 5 | 接下来我们主要演示如何转换异步回调接口,以及实现一些依赖调度器的实用组件。 6 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/futuretaskyu-fork.md: -------------------------------------------------------------------------------- 1 | ## FutureTask与fork 2 | 3 | 在多线程代码中我们经常会遇到这种模型,将一个耗时任务, new一个新的Thread或者通常放到线程池后台执行,当前线程执行另外任务,之后通过某个api接口阻塞获取后台任务结果。 4 | 5 | Java童鞋应该对这个概念非常熟悉——JDK给予直接支持的Future。 6 | 7 | 同样的模型我们可以利用channel对多个协程进行同步来实现,代码很简单: 8 | 9 | 10 | ```php 11 | send(42); // 通知+传送结果 22 | }); 23 | 24 | yield delay(500); 25 | $r = (yield $ch->recv()); // 阻塞等待结果 26 | echo $r; // 42 27 | 28 | // 我们这里两个耗时任务并发执行,总耗时约1000ms 29 | echo "cost ", microtime(true) - $start, "\n"; 30 | }); 31 | 32 | ``` 33 | 34 | 事实上我们也很容易把Future模型移植过来: 35 | 36 | ```php 37 | state = self::PENDING; 51 | 52 | $asyncTask = new AsyncTask($gen); 53 | $asyncTask->begin(function($r, $ex = null) { 54 | $this->state = self::DONE; 55 | if ($cc = $this->cc) { 56 | // 有cc,说明有call get方法挂起协程,在此处唤醒 57 | $cc($r, $ex); 58 | } else { 59 | // 无挂起,暂存执行结果 60 | $this->result = $r; 61 | $this->ex = $ex; 62 | } 63 | }); 64 | } 65 | 66 | public function get() 67 | { 68 | return callcc(function($cc) use($timeout) { 69 | if ($this->state === self::DONE) { 70 | // 获取结果时,任务已经完成,同步返回结果 71 | // 这里也可以考虑用defer实现,异步返回结果,首先先释放php栈,降低内存使用 72 | $cc($this->result, $this->ex); 73 | } else { 74 | // 获取结果时未完成,保存$cc,挂起等待 75 | $this->cc = $cc; 76 | } 77 | }); 78 | } 79 | } 80 | 81 | 82 | // helper 83 | function fork($task, ...$args) 84 | { 85 | $task = await($task); // 将task转换为生成器 86 | yield new FutureTask($task); 87 | } 88 | ``` 89 | 90 | 91 | 还是刚才那个例子, 我们改写为FutureTask版本: 92 | 93 | 94 | ```php 95 | get()); 110 | echo $r; // 42 111 | 112 | // 总耗时仍旧只有1000ms 113 | echo "cost ", microtime(true) - $start, "\n"; 114 | }); 115 | ``` 116 | 117 | 118 | 再进一步,我们扩充FutureTask的状态,阻塞获取结果加入超时选项: 119 | 120 | 121 | ```php 122 | state = self::PENDING; 142 | 143 | if ($parent) { 144 | $asyncTask = new AsyncTask($gen, $parent); 145 | } else { 146 | $asyncTask = new AsyncTask($gen); 147 | } 148 | 149 | $asyncTask->begin(function($r, $ex = null) { 150 | // PENDING or TIMEOUT 151 | if ($this->state === self::TIMEOUT) { 152 | return; 153 | } 154 | 155 | // PENDING -> DONE 156 | $this->state = self::DONE; 157 | 158 | if ($cc = $this->cc) { 159 | if ($this->timerId) { 160 | swoole_timer_clear($this->timerId); 161 | } 162 | $cc($r, $ex); 163 | } else { 164 | $this->result = $r; 165 | $this->ex = $ex; 166 | } 167 | }); 168 | } 169 | 170 | // 这里超时时间0为永远阻塞, 171 | // 否则超时未获取到结果,将向父任务传递超时异常 172 | public function get($timeout = 0) 173 | { 174 | return callcc(function($cc) use($timeout) { 175 | // PENDING or DONE 176 | if ($this->state === self::DONE) { 177 | $cc($this->result, $this->ex); 178 | } else { 179 | // 获取结果时未完成,保存$cc,开启定时器(如果需要),挂起等待 180 | $this->cc = $cc; 181 | $this->getResultTimeout($timeout); 182 | } 183 | }); 184 | } 185 | 186 | private function getResultTimeout($timeout) 187 | { 188 | if (!$timeout) { 189 | return; 190 | } 191 | 192 | $this->timerId = swoole_timer_after($timeout, function() { 193 | assert($this->state === self::PENDING); 194 | $this->state = self::TIMEOUT; 195 | $cc = $this->cc; 196 | $cc(null, new AsyncTimeoutException()); 197 | }); 198 | } 199 | } 200 | ``` 201 | 202 | 因为引入parentTask参数,需要将父任务隐式传参,而我们执行通过Syscall与执行当前生成器的父任务交互,所以我们重写fork辅助函数,改用Syscall实现: 203 | 204 | 205 | ```php 206 | get(100)); 240 | var_dump($r); 241 | } catch (\Exception $ex) { 242 | echo "get result timeout\n"; 243 | } 244 | 245 | yield delay(1000); 246 | 247 | // 因为我们只等待子任务100ms,我们的总耗时只有 1100ms 248 | echo "cost ", microtime(true) - $start, "\n"; 249 | }); 250 | 251 | go(function() { 252 | $start = microtime(true); 253 | 254 | /** @var $future FutureTask */ 255 | $future = (yield fork(function() { 256 | yield delay(500); 257 | yield 42; 258 | throw new \Exception(); 259 | })); 260 | 261 | yield delay(1000); 262 | 263 | // 子任务500ms前发生异常,已经处于完成状态 264 | // 我们调用get会当即引发异常 265 | try { 266 | $r = (yield $future->get()); 267 | var_dump($r); 268 | } catch (\Exception $ex) { 269 | echo "something wrong in child task\n"; 270 | } 271 | 272 | // 因为耗时任务并发执行,这里总耗时仅1000ms 273 | echo "cost ", microtime(true) - $start, "\n"; 274 | }); 275 | 276 | ``` -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/huan-cun-channel.md: -------------------------------------------------------------------------------- 1 | ## 缓存channel 2 | 3 | 接下来我们来实现带缓存的Channel: 4 | 5 | > Sends to a buffered channel block only when the buffer is full. 6 | 7 | > Receives block when the buffer is empty. 8 | 9 | 10 | ```php 11 | 0); 30 | $this->cap = $cap; 31 | $this->queue = new \SplQueue(); 32 | $this->sendCc = new \SplQueue(); 33 | $this->recvCc = new \SplQueue(); 34 | } 35 | 36 | public function recv() 37 | { 38 | return callcc(function($cc) { 39 | if ($this->queue->isEmpty()) { 40 | 41 | // 当无数据可接收时,$cc入列,让出控制流,挂起接收者协程 42 | $this->recvCc->enqueue($cc); 43 | 44 | } else { 45 | 46 | // 当有数据可接收时,先接收数据,然后恢复控制流 47 | $val = $this->queue->dequeue(); 48 | $this->cap++; 49 | $cc($val, null); 50 | 51 | } 52 | 53 | // 递归唤醒其他被阻塞的发送者与接收者收发数据,注意顺序 54 | $this->recvPingPong(); 55 | }); 56 | } 57 | 58 | public function send($val) 59 | { 60 | return callcc(function($cc) use($val) { 61 | if ($this->cap > 0) { 62 | 63 | // 当缓存未满,发送数据直接加入缓存,然后恢复控制流 64 | $this->queue->enqueue($val); 65 | $this->cap--; 66 | $cc(null, null); 67 | 68 | } else { 69 | 70 | // 当缓存满,发送者控制流与发送数据入列,让出控制流,挂起发送者协程 71 | $this->sendCc->enqueue([$cc, $val]); 72 | 73 | } 74 | 75 | // 递归唤醒其他被阻塞的接收者与发送者收发数据,注意顺序 76 | $this->sendPingPong(); 77 | 78 | // 如果全部代码都为同步,防止多个发送者时,数据全部来自某个发送者 79 | // 应该把sendPingPong 修改为异步执行 defer([$this, "sendPingPong"]); 80 | // 但是swoole本身的defer实现有bug,除非把defer 实现为swoole_timer_after(1, ...) 81 | // recvPingPong 同理 82 | }); 83 | } 84 | 85 | public function recvPingPong() 86 | { 87 | // 当有阻塞的发送者,唤醒其发送数据 88 | if (!$this->sendCc->isEmpty() && $this->cap > 0) { 89 | list($sendCc, $val) = $this->sendCc->dequeue(); 90 | $this->queue->enqueue($val); 91 | $this->cap--; 92 | $sendCc(null, null); 93 | 94 | // 当有阻塞的接收者,唤醒其接收数据 95 | if (!$this->recvCc->isEmpty() && !$this->queue->isEmpty()) { 96 | $recvCc = $this->recvCc->dequeue(); 97 | $val = $this->queue->dequeue(); 98 | $this->cap++; 99 | $recvCc($val); 100 | 101 | $this->recvPingPong(); 102 | } 103 | } 104 | } 105 | 106 | public function sendPingPong() 107 | { 108 | // 当有阻塞的接收者,唤醒其接收数据 109 | if (!$this->recvCc->isEmpty() && !$this->queue->isEmpty()) { 110 | $recvCc = $this->recvCc->dequeue(); 111 | $val = $this->queue->dequeue(); 112 | $this->cap++; 113 | $recvCc($val); 114 | 115 | // 当有阻塞的发送者,唤醒其发送数据 116 | if (!$this->sendCc->isEmpty() && $this->cap > 0) { 117 | list($sendCc, $val) = $this->sendCc->dequeue(); 118 | $this->queue->enqueue($val); 119 | $this->cap--; 120 | $sendCc(null, null); 121 | 122 | $this->sendPingPong(); 123 | } 124 | } 125 | } 126 | } 127 | ``` -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/qi-ta-you-qu-de-zu-jian.md: -------------------------------------------------------------------------------- 1 | ## 其他有趣的组件 2 | 3 | 异步回调API是无法直接使用yield语法的,需要使用thunk或者promise进行转换,thunkfy就是将多参数函数替换成单参数函数且参数只接受回调(参见:[Thunk 函数的含义和用法](http://www.ruanyifeng.com/blog/2015/05/thunk.html))。上文中我们将回调API显式实现Async接口,显得有些麻烦,这里可以把 **“通过参数传递异步结果回调度器”** 这个模式抽象出来,实现一个穷人的call/cc。 -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/raceyu-timeout.md: -------------------------------------------------------------------------------- 1 | ## race与timeout 2 | 3 | 看到这里,你可能已经发现到我们封装的异步接口的问题了: 没有任何超时处理。 4 | 5 | 通常情况我们会为每个异步调用添加定时器,回调成功取消定时器,否则在定时器回调透传异常,例如: 6 | 7 | ```php 8 | 0) { 42 | $fun = timeoutWrapper($fun, $timeout); 43 | } 44 | return new CallCC($fun); 45 | } 46 | 47 | 48 | // 我们的dns查询有了超时透传异常的能力了 49 | function async_dns_lookup($host, $timeout = 100) 50 | { 51 | return callcc(function($k) use($host) { 52 | swoole_async_dns_lookup($host, function($host, $ip) use($k) { 53 | $k($ip); 54 | }); 55 | }, $timeout); 56 | } 57 | 58 | 59 | spawn(function() { 60 | try { 61 | yield async_dns_lookup("www.xxx.com", 1); 62 | } catch (\Exception $ex) { 63 | echo $ex; // ex! 64 | } 65 | }); 66 | 67 | ``` 68 | 69 | 但是,我们可以有更优雅通用的方式来超时处理: 70 | 71 | 72 | ```php 73 | tasks = $tasks; 86 | $this->parent = $parent; 87 | $this->done = false; 88 | } 89 | 90 | public function begin(callable $continuation) 91 | { 92 | $this->continuation = $continuation; 93 | foreach ($this->tasks as $id => $task) { 94 | (new AsyncTask($task, $this->parent))->begin($this->continuation($id)); 95 | }; 96 | } 97 | 98 | private function continuation($id) 99 | { 100 | return function($r, $ex = null) use($id) { 101 | if ($this->done) { 102 | return; 103 | } 104 | $this->done = true; 105 | 106 | if ($this->continuation) { 107 | $k = $this->continuation; 108 | $k($r, $ex); 109 | } 110 | }; 111 | } 112 | } 113 | ``` 114 | 115 | ```php 116 | awaitGet("/"), 184 | timeout(200), 185 | ])); 186 | var_dump($res->statusCode); 187 | } catch (\Exception $ex) { 188 | echo $ex; 189 | swoole_event_exit(); 190 | } 191 | }); 192 | ``` 193 | 194 | 我们非常容易构造出更多支持超时的接口, 但我们代码看起来比之前更加清晰; 195 | 196 | 197 | ```php 198 | get($uri, $k); 207 | }), 208 | timeout($timeout), 209 | ]); 210 | } 211 | // ... 212 | } 213 | 214 | 215 | function async_dns_lookup($host, $timeout = 100) 216 | { 217 | return race([ 218 | callcc(function($k) use($host) { 219 | swoole_async_dns_lookup($host, function($host, $ip) use($k) { 220 | $k($ip); 221 | }); 222 | }), 223 | timeout($timeout), 224 | ]); 225 | } 226 | 227 | 228 | // test 229 | spawn(function() { 230 | try { 231 | $ip = (yield race([ 232 | async_dns_lookup("www.baidu.com"), 233 | timeout(100), 234 | ])); 235 | $res = (yield (new HttpClient($ip, 80))->awaitGet("/")); 236 | var_dump($res->statusCode); 237 | } catch (\Exception $ex) { 238 | echo $ex; 239 | swoole_event_exit(); 240 | } 241 | }); 242 | 243 | ``` 244 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/returngai-xie.md: -------------------------------------------------------------------------------- 1 | ## 改写return 2 | 3 | return其实可以被替换为单参数且永远不返回的函数,将return解糖进行CPS变换,改写为函数参数continuation,将迭代result作为回调参数返回,为引入异步迭代做准备。 4 | 5 | ```php 6 | continuation = $continuation; 15 | $this->next(); 16 | } 17 | 18 | public function next($result = null) 19 | { 20 | $value = $this->gen->send($result); 21 | 22 | if ($this->gen->valid()) { 23 | if ($value instanceof \Generator) { 24 | // 父任务next方法是子任务的延续, 25 | // 子任务迭代完成后继续完成父任务迭代 26 | $continuation = [$this, "next"]; 27 | (new self($value))->begin($continuation); 28 | } else { 29 | $this->next($value); 30 | } 31 | 32 | } else { 33 | $cc = $this->continuation; 34 | $cc($result); 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | ```php 41 | begin($trace); // output: 123 53 | 54 | ``` -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-die-dai.md: -------------------------------------------------------------------------------- 1 | ## 生成器迭代 2 | 3 | 手动迭代生成器,递归执行 `AsyncTask::next`,调用`Generator::send`方法将将yield值作为yield表达式结果。 4 | 5 | 1. yield表达式可能是一个异步调用,我们这里为之后把异步调用的结果作为yield表达式结果铺垫。 6 | 2. yield外侧括号在PHP5必须,PHP7不需要。 7 | 8 | ``` 9 | 如, $ip = (yield async_dns_lookup(...) ); 10 | ^ |--------------------| 11 | | yield值 12 | | |---------------------------| 13 | yield表达式结果 yield 表达式 14 | ``` 15 | 16 | 17 | ```php 18 | gen = new Gen($gen); 27 | } 28 | 29 | public function begin() 30 | { 31 | $this->next(); 32 | } 33 | 34 | public function next($result = null) 35 | { 36 | $value = $this->gen->send($result); 37 | if ($this->gen->valid()) { 38 | $this->next($value); 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | ```php 45 | begin(); // output: 12 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-fan-hui-zhi.md: -------------------------------------------------------------------------------- 1 | ## 生成器返回值 2 | 3 | PHP7支持通过[`Generator::getReturn`](http://php.net/manual/en/generator.getreturn.php)获取生成器方法return的返回值。 4 | 5 | **PHP5中我们约定使用Generator最后一次yield值作为返回值。** 6 | 7 | ```php 8 | next(); 15 | } 16 | 17 | // 添加return传递每一次迭代的结果,直到向上传递到begin 18 | public function next($result = null) 19 | { 20 | $value = $this->gen->send($result); 21 | 22 | if ($this->gen->valid()) { 23 | return $this->next($value); 24 | } else { 25 | return $result; 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | ```php 32 | begin(); // output: 12 42 | echo $r; // output: 3 43 | ``` -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi-wei-tuo.md: -------------------------------------------------------------------------------- 1 | ## 生成器委托 2 | 3 | PHP7中支持[`delegating generator`](https://wiki.php.net/rfc/generator-delegation),可以自动展开`subgenerator`; 4 | 5 | > A “subgenerator” is a Generator used in theportion of the yield from syntax. 6 | 7 | 我们需要在PHP5支持子生成器,将子生成器最后yield值作为父生成器yield表达式结果,仅只需要加两行代码,递归的产生一个`AsyncTask`对象来执行子生成器即可。 8 | 9 | ```php 10 | gen->send($result); 17 | 18 | if ($this->gen->valid()) { 19 | if ($value instanceof \Generator) { 20 | $value = (new self($value))->begin(); 21 | } 22 | return $this->next($value); 23 | 24 | } else { 25 | return $result; 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | ```php 32 | begin(); // output: 12 48 | echo $r; // output: 3 49 | 50 | ``` 51 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/sheng-cheng-qi.md: -------------------------------------------------------------------------------- 1 | ## 统一生成器接口 2 | 3 | 由于内部隐式rewind,需要先调用`Generator::current`获取当前value,而直接调用`Generator::send`会跳到第二次yield。 4 | 5 | 1. [send方法参考](http://php.net/manual/en/generator.send.php) 6 | 2. [生成器参考](http://www.laruence.com/2015/05/28/3038.html) 7 | 8 | 9 | ```php 10 | generator = $generator; 20 | } 21 | 22 | public function valid() 23 | { 24 | return $this->generator->valid(); 25 | } 26 | 27 | public function send($value = null) 28 | { 29 | if ($this->isfirst) { 30 | $this->isfirst = false; 31 | return $this->generator->current(); 32 | } else { 33 | return $this->generator->send($value); 34 | } 35 | } 36 | } 37 | ``` -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/spawn.md: -------------------------------------------------------------------------------- 1 | ## spawn 2 | 3 | 为了易用性,我们为`AsyncTask`的创建一个可灵活传递参数函数入口。 4 | 5 | ```php 6 | $v) { 56 | $task->$k = $v; 57 | } 58 | (new AsyncTask($task, $parent))->begin($continuation); 59 | } else { 60 | $continuation($task, null); 61 | } 62 | } 63 | ``` -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/syscallyu-context.md: -------------------------------------------------------------------------------- 1 | ## `Syscall`与`Context` 2 | 3 | 按照nikic的思路引入与调度器内部交互的`Syscall`,将需要执行的函数打包成`Syscall`,通过yield返回迭代器,可以从`Syscall`参数获取到当前迭代器对象,这里提供了一个外界与AsyncTask交互的扩展点。 4 | 5 | 我们借此演示如何添加跨生成器上下文,在嵌套生成器共享数据,解耦生成器之间依赖。 6 | 7 | ```php 8 | gen = new Gen($gen); 20 | $this->parent = $parent; 21 | } 22 | 23 | public function begin(callable $continuation) 24 | { 25 | $this->continuation = $continuation; 26 | $this->next(); 27 | } 28 | 29 | public function next($result = null, $ex = null) 30 | { 31 | try { 32 | if ($ex) { 33 | $value = $this->gen->throw_($ex); 34 | } else { 35 | $value = $this->gen->send($result); 36 | } 37 | 38 | if ($this->gen->valid()) { 39 | // 这里注意优先级, Syscall 可能返回\Generator 或者 Async 40 | if ($value instanceof Syscall) { // Syscall 签名见下方 41 | $value = $value($this); 42 | } 43 | 44 | if ($value instanceof \Generator) { 45 | $value = new self($value, $this); 46 | } 47 | 48 | if ($value instanceof Async) { 49 | $cc = [$this, "next"]; 50 | $value->begin($cc); 51 | } else { 52 | $this->next($value, null); 53 | } 54 | } else { 55 | $cc = $this->continuation; 56 | $cc($result, null); 57 | } 58 | } catch (\Exception $ex) { 59 | if ($this->gen->valid()) { 60 | $this->next(null, $ex); 61 | } else { 62 | $cc = $this->continuation; 63 | $cc($result, $ex); 64 | } 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | Syscall将 (callable :: AsyncTask $task -> mixed) 包装成单独类型: 71 | 72 | ```php 73 | fun = $fun; 81 | } 82 | 83 | public function __invoke(AsyncTask $task) 84 | { 85 | $cb = $this->fun; 86 | return $cb($task); 87 | } 88 | } 89 | ``` 90 | 91 | 因为PHP对象属性为Hashtable实现,而生成器对象本身无任何属性,我们这里把`Context`的KV数据附加到根生成器对象上,然后得到的Context的Get与Set函数: 92 | 93 | ```php 94 | parent && $task = $task->parent); 99 | if (isset($task->gen->generator->$key)) { 100 | return $task->gen->generator->$key; 101 | } else { 102 | return $default; 103 | } 104 | }); 105 | } 106 | 107 | function setCtx($key, $val) 108 | { 109 | return new Syscall(function(AsyncTask $task) use($key, $val) { 110 | while($task->parent && $task = $task->parent); 111 | $task->gen->generator->$key = $val; 112 | }); 113 | } 114 | ``` 115 | 116 | ```php 117 | begin($trace); // output: bar 132 | 133 | ``` -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/wu-huancun-channel.md: -------------------------------------------------------------------------------- 1 | ## 无缓存channel 2 | 3 | 我们首先实现无缓存的Channel: 4 | 5 | 6 | ```php 7 | recvQ = new \SplQueue(); 21 | $this->sendQ = new \SplQueue(); 22 | } 23 | 24 | public function send($val) 25 | { 26 | return callcc(function($cc) use($val) { 27 | if ($this->recvQ->isEmpty()) { 28 | 29 | // 当chan没有接收者,发送者协程挂起(将$cc入列,不调用$cc回送数据) 30 | $this->sendQ->enqueue([$cc, $val]); 31 | 32 | } else { 33 | 34 | // 当chan对端有接收者,将挂起接收者协程出列, 35 | // 调用接收者$recvCc发送数据,运行接收者协程后继代码 36 | // 执行完毕或者遇到Async挂起,$recvCc()调用返回, 37 | // 调用$cc(),控制流回到发送者协程 38 | $recvCc = $this->recvQ->dequeue(); 39 | $recvCc($val, null); 40 | $cc(null, null); 41 | 42 | } 43 | }); 44 | } 45 | 46 | public function recv() 47 | { 48 | return callcc(function($cc) { 49 | if ($this->sendQ->isEmpty()) { 50 | 51 | // 当chan没有发送者,接收者协程挂起(将$cc入列) 52 | $this->recvQ->enqueue($cc); 53 | 54 | } else { 55 | 56 | // 当chan对端有发送者,将挂起发送者协程与待发送数据出列 57 | // 调用发送者$sendCc发送数据,运行发送者协程后继代码 58 | // 执行完毕或者遇到Async挂起,$sendCc()调用返回, 59 | // 调用$cc(),控制流回到接收者协程 60 | list($sendCc, $val) = $this->sendQ->dequeue(); 61 | $sendCc(null, null); 62 | $cc($val, null); 63 | 64 | } 65 | }); 66 | } 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-chuan-di-liu-cheng.md: -------------------------------------------------------------------------------- 1 | ## 异常: 传递流程 2 | 3 | 基于上述注释观察并理解异常传递流程: 4 | 5 | ```php 6 | b2 -> 14 | (new AsyncTask(g1()))->begin(); 15 | 16 | 17 | function g2() 18 | { 19 | yield; 20 | throw new \Exception(); 21 | } 22 | // a2 (-> a4 -> a2) -> b2 -> b2 -> 23 | (new AsyncTask(g2()))->begin(); 24 | 25 | 26 | function g3() 27 | { 28 | yield; 29 | throw new \Exception(); 30 | } 31 | // a2 (-> a4 -> a2) -> b2 -> b2 -> 32 | (new AsyncTask(g3()))->begin(); 33 | 34 | 35 | function g4() 36 | { 37 | yield; 38 | yield; 39 | throw new \Exception(); 40 | } 41 | // a2 (-> a4 -> a2) (-> a4 -> a2) -> b2 -> b2 -> b2 -> 42 | (new AsyncTask(g4()))->begin(); 43 | 44 | 45 | function g5() 46 | { 47 | throw new \Exception(); 48 | /** @noinspection PhpUnreachableStatementInspection */ 49 | yield; 50 | } 51 | function g7() 52 | { 53 | yield g5(); 54 | } 55 | // (a2 -> a3) -> a2 (-> b2 -> b1 -> c) -> b2 -> 56 | (new AsyncTask(g7()))->begin(); 57 | 58 | 59 | function g6() 60 | { 61 | yield; 62 | throw new \Exception(); 63 | } 64 | function g8() 65 | { 66 | yield g6(); 67 | } 68 | // (a2 -> a3) -> a2 -> a4 -> a2 -> b2 -> b2 -> b1 -> c -> b2 -> 69 | (new AsyncTask(g8()))->begin(); 70 | 71 | 72 | function g9() 73 | { 74 | try { 75 | yield g5(); 76 | } catch (\Exception $ex) { 77 | 78 | } 79 | } 80 | // a2 -> a3 -> a2 -> b2 -> b1 -> c -> 81 | (new AsyncTask(g9()))->begin(); 82 | ``` 83 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-qian-tao-ren-wu-tou-chuan.md: -------------------------------------------------------------------------------- 1 | ## 异常: 嵌套任务透传 2 | 3 | 重新处理生成器嵌套,需要将子生成器异常抛向父生成器。 4 | 5 | 当生成器迭代过程发生未捕获异常,生成器将会被关闭,`Generator::valid`返回false,未捕获异常会从生成器内部被抛向父作用域,嵌套子生成器内部的未捕获异常必须最终被抛向根生成器的calling frame,PHP7中`yield-from`对嵌套子生成器resume时产生的异常,采取`goto try_again传递`+`while`方式层层向上抛出,我们的代码因为递归迭代的原因,未捕获异常需要逆递归栈帧方向层层上抛,性能方便有改进余地。 6 | 7 | ```php 8 | "; 20 | $value = $this->gen->throw_($ex); 21 | } else { 22 | // a2. 当前生成器可能抛出异常 23 | // echo "a2 -> "; 24 | $value = $this->gen->send($result); 25 | } 26 | 27 | if ($this->gen->valid()) { 28 | if ($value instanceof \Generator) { 29 | // a3. 子生成器可能抛出异常 30 | // echo "a3 -> "; 31 | $value = (new self($value))->begin(); 32 | } 33 | // echo "a4 -> "; 34 | return $this->next($value); 35 | } else { 36 | return $result; 37 | } 38 | } catch (\Exception $ex) { 39 | // !! 当生成器迭代过程发生未捕获异常, 生成器将会被关闭, valid()返回false, 40 | if ($this->gen->valid()) { 41 | // b1. 所以, 当前分支的异常一定不是当前生成器所抛出, 而是来自嵌套的子生成器 42 | // 此处将子生成器异常通过(c)向当前生成器抛出异常 43 | // echo "b1 -> "; 44 | return $this->next(null, $ex); 45 | } else { 46 | // b2. 逆向(递归栈帧)方向向上抛 或者 向父生成器(如果存在)抛出异常 47 | // echo "b2 -> "; 48 | throw $ex; 49 | } 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | ```php 56 | getMessage(); 70 | } 71 | $r2 = (yield 2); 72 | yield 3; 73 | } 74 | $task = new AsyncTask(newGen()); 75 | $r = $task->begin(); // output: e 76 | echo $r; // output: 3 77 | 78 | ``` 79 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383a-zhong-xin-jin-xing-cps-bian-huan.md: -------------------------------------------------------------------------------- 1 | ## 异常: 重新进行CPS变换 2 | 3 | 我们把加入异常处理的代码重新修改为CPS方式: 4 | 5 | ```php 6 | continuation = $continuation; 14 | $this->next(); 15 | } 16 | 17 | public function next($result = null, \Exception $ex = null) 18 | { 19 | try { 20 | if ($ex) { 21 | $value = $this->gen->throw_($ex); 22 | } else { 23 | $value = $this->gen->send($result); 24 | } 25 | 26 | if ($this->gen->valid()) { 27 | if ($value instanceof \Generator) { 28 | // 注意这里 29 | $continuation = [$this, "next"]; 30 | (new self($value))->begin($continuation); 31 | } else { 32 | $this->next($value); 33 | } 34 | } else { 35 | // 迭代结束 返回结果 36 | $cc = $this->continuation; // cc指向 父生成器next方法 或 用户传入continuation 37 | $cc($result, null); 38 | } 39 | } catch (\Exception $ex) { 40 | if ($this->gen->valid()) { 41 | // 抛出异常 42 | $this->next(null, $ex); 43 | } else { 44 | // 未捕获异常 45 | $cc = $this->continuation; // cc指向 父生成器next方法 或 用户传入continuation 46 | $cc(null, $ex); 47 | } 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | ```php 54 | getMessage(); // output: e 70 | } else { 71 | echo $r; 72 | } 73 | }; 74 | $task->begin($trace); 75 | ``` 76 | 77 | ```php 78 | getMessage(); // output: e 92 | } 93 | $r2 = (yield 2); 94 | yield 3; 95 | } 96 | $task = new AsyncTask(newGen()); 97 | $trace = function($r, $ex) { 98 | if ($ex) { 99 | echo $ex->getMessage(); 100 | } else { 101 | echo $r; // output: 3 102 | } 103 | }; 104 | $task->begin($trace); // output: e 105 | 106 | ``` -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yi-5e383azhong-xin-jia-ru-async.md: -------------------------------------------------------------------------------- 1 | ## 异常: 重新加入Async 2 | 3 | 重新加入Async,修改continuation的签名,加入异常参数: 4 | 5 | ```php 6 | void 10 | public function begin(callable $continuation); 11 | } 12 | 13 | final class AsyncTask implements Async 14 | { 15 | public function next($result = null, $ex = null) 16 | { 17 | try { 18 | // ... 19 | if ($this->gen->valid()) { 20 | if ($value instanceof \Generator) { 21 | $value = new self($value); 22 | } 23 | 24 | if ($value instanceof Async) { 25 | $cc = [$this, "next"]; 26 | $value->begin($cc); 27 | } else { 28 | $this->next($value, null); 29 | } 30 | } else { 31 | $cc = $this->continuation; 32 | $cc($result, null); 33 | } 34 | } catch (\Exception $ex) { 35 | // ... 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | ```php 42 | getMessage(), "\n"; 46 | } 47 | }; 48 | 49 | class AsyncException implements Async 50 | { 51 | public function begin(callable $cc) 52 | { 53 | swoole_timer_after(1000, function() use($cc) { 54 | $cc(null, new \Exception("timeout")); 55 | }); 56 | } 57 | } 58 | 59 | function newSubGen() 60 | { 61 | yield 0; 62 | $async = new AsyncException(); 63 | yield $async; 64 | } 65 | 66 | function newGen($try) 67 | { 68 | $start = time(); 69 | try { 70 | $r1 = (yield newSubGen()); 71 | } catch (\Exception $ex) { 72 | // 捕获subgenerator抛出的异常 73 | if ($try) { 74 | echo "catch:" . $ex->getMessage(), "\n"; 75 | } else { 76 | throw $ex; 77 | } 78 | } 79 | echo time() - $start, "\n"; 80 | } 81 | 82 | // 内部try-catch异常 83 | $task = new AsyncTask(newGen(true)); 84 | $task->begin($trace); 85 | // output: 86 | // catch:timeout 87 | // 1 88 | 89 | // 异常传递至AsyncTask的最终回调 90 | $task = new AsyncTask(newGen(false)); 91 | $task->begin($trace); 92 | // output: 93 | // cc_ex:timeout 94 | ``` 95 | -------------------------------------------------------------------------------- /di-yi-bu-52063a-ban-xie-cheng-diao-du-qi/yin-ru-yi-chang-chu-li.md: -------------------------------------------------------------------------------- 1 | ## 引入异常处理 2 | 3 | 尽管只有寥寥几行代码,我们却已经实现了可工作的半协程调度器(缺失异常处理)。 4 | 5 | 没关系,下面先rollback回return的实现,开始引入异常处理,目标是在嵌套生成器之间正确向上抛出异常,跨生成器捕获异常。 6 | 7 | ```php 8 | generator->throw($ex); 17 | } 18 | } 19 | 20 | final class AsyncTask 21 | { 22 | public function begin() 23 | { 24 | return $this->next(); 25 | } 26 | 27 | // 这里添加第二个参数, 用来在迭代过程传递异常 28 | public function next($result = null, \Exception $ex = null) 29 | { 30 | if ($ex) { 31 | $this->gen->throw_($ex); 32 | } 33 | 34 | $ex = null; 35 | try { 36 | // send方法内部是一个resume的过程: 37 | // 恢复execute_data上下文, 调用zend_execute_ex()继续执行, 38 | // 后续中op_array内可能会抛出异常 39 | $value = $this->gen->send($result); 40 | } catch (\Exception $ex) {} 41 | 42 | if ($ex) { 43 | if ($this->gen->valid()) { 44 | // 传递异常 45 | return $this->next(null, $ex); 46 | } else { 47 | throw $ex; 48 | } 49 | } else { 50 | if ($this->gen->valid()) { 51 | // 正常yield值 52 | return $this->next($value); 53 | } else { 54 | return $result; 55 | } 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | ```php 62 | begin(); 73 | echo $r; 74 | } catch (\Exception $ex) { 75 | echo $ex->getMessage(); // output: e 76 | } 77 | ``` -------------------------------------------------------------------------------- /fu-lu.md: -------------------------------------------------------------------------------- 1 | # 附录 2 | 3 | 个人对yield与coroutine的理解与总结,有问题欢迎指正。 4 | 5 | 在上文半协程中: 6 | 7 | 1. 从抽象角度可以将「yield」理解成为CPS变换的语法糖,yield帮我们优雅的链接了异步任务序列; 8 | 2. 从控制流角度可以将「yield」理解为将程序控制权从callee(Generator)转移到caller,只有借由底层eventloop驱动,将控制权重新转移回Generator; 9 | 3. 从实现角度来看「yield」,每个生成器对象都会有自己的zend_execute_data与zend_vm_stack,调用send\next\throw方法,都需要首先备份EG中相关上下文,然后将Generator的execute_data信息恢复到EG,然后调用zend_execute_ex()从从当前上下文恢复执行执行,之后最后恢复执行前EG信息; 10 | 4. 因为ZendVM中stack与execute_data的保存与切换工作已经由Generator实现了,基于Generator构建半协程的核心问题是控制流转换; 11 | 5. yield」并没有消除回调,只是让代码从视觉上变成同步,实际仍异步执行,事实上任何异步模型,底层最后都是基于回调的。 -------------------------------------------------------------------------------- /php-co-koa.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youzan/php-co-koa/6e18a79cb6f52f288a10d51e92ae833eab098189/php-co-koa.pdf -------------------------------------------------------------------------------- /qian-yan.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 近年来,在面向高并发编程的道路上,Node.js与Golang风生水起,让人们渐渐把目光从多线程模型转移到callback与CSP/Actor上,用惯了FPM多进程同步阻塞模型的PHPer中总难免有人心 4 | 动。多种EventLoop一直不温不火,而国内以swoole为代表,直接以扩展形式,提供了整套callback模型的PHP异步编程解决方案,正在逐渐的流行起来。 5 | 6 | Node.js在JS上开花结果,也许是浏览器的DOM事件模型培养起来的callback书写习惯,与语言自身的函数式特性适合callback代码编写。但回调固有的逻辑割裂、调试维护难的问题随着node社区的繁荣逐渐显现,从老赵脑洞大开的windjs到co与Promise,方案层出不穷,最终[Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)被 7 | 采纳为官方「异步编程标准规范」,从C#借鉴过来的[async/await](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function)被纳入语言标准。 8 | 9 | 因swoole与Node.js的I/O模型相同,PHPer有幸在高并发命题上遭遇与node一样的问题。Closure([RFC](https://wiki.php.net/rfc/closures?cm_mc_uid=26754990333314676210612&cm_mc_sid_50200000=1490031947))一定程度从语言本身改善了异步编程的体验,受限于Zend引擎作用域实现机制,PHP因缺失词法作用域从而缺失词法闭包,Closure对象采用了use语法来显式捕获upValue到静态属性的方式(closure->func.op_array.static_variables),我个人认为这有点像无法自动实现闭包的匿名函数。之后Nikita Popov在PHP中实现了Generator([RFC](https://wiki.php.net/rfc/generators)),并且让PHPer意 10 | 识到生成器原来可以实现实现非抢占任务调度([译文:在PHP中使用协程实现多任务调度](http://www.laruence.com/2015/05/28/3038.html))。我们最终可以借助于生成器实现半协程来解决该问题。 11 | 12 | 这篇文章秉承着造轮子的精神,我们从头实现一个全功能的基于生成器(Generator)的半协程调度器与相关基础组件,并基于该调度器实(chao)现(xi)JS社区当红的koa框架,最终加深我们对异步编程的理解。 -------------------------------------------------------------------------------- /qian-yan/shuo-ming.md: -------------------------------------------------------------------------------- 1 | ## 说明 2 | 3 | 1. 下文中协程均指代使用生成器实现的半协程,具体概念参见[Wiki](https://en.wikipedia.org/wiki/Coroutine)。 4 | 2. 下文中耗时任务指代I/O或定时器,非CPU计算。 5 | 3. `广告` 继TSF之后,我司去年了开源[Zan Framework](http://zanphp.io/),内部的半协程调度器已经解决了swoole中回调接口的代码书写问题。 6 | 4. 下文实例代码,限于篇幅,每部分仅呈现改动部分, 其余省略。 -------------------------------------------------------------------------------- /shuo-ming.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youzan/php-co-koa/6e18a79cb6f52f288a10d51e92ae833eab098189/shuo-ming.md --------------------------------------------------------------------------------