├── img ├── 404-bg.jpg ├── favicon.ico ├── home-bg.jpg ├── tag-bg.jpg ├── contact-bg.jpg ├── home-bg-o.jpg ├── icon_wechat.png ├── piasy_avatar.jpg └── we_chat_mp_qrcode.jpg ├── fonts ├── fontawesome-webfont.woff ├── fontawesome-webfont.woff2 ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── .gitignore ├── less ├── variables.less ├── sidebar.less └── mixins.less ├── archive.md ├── README.md ├── _layouts ├── default.html └── page.html ├── package.json ├── _posts ├── 2016-10-06-operator-internals-introduction.md ├── 2017-06-18-the-reactive-scrabble-benchmarks.md ├── 2017-02-24-operator-internals-autoconnect.md ├── 2016-10-16-operator-internals-all.md ├── 2016-10-06-operator-internals-s-amb-ambwith.md ├── 2016-06-18-operator-concurrency-primitives-6.md ├── 2016-07-29-operator-concurrency-primitives-subscription-containers-3.md ├── 2016-06-25-pitfalls-of-operator-implementations-2.md ├── 2016-08-19-schedulers-2.md ├── 2016-10-02-the-reactive-streams-api-part-4.md ├── 2016-08-05-schedulers-1.md ├── 2016-09-10-the-reactive-streams-api-part-1.md ├── 2016-09-02-schedulers-4.md ├── 2016-07-15-operator-concurrency-primitives-subscription-containers-1.md ├── 2017-05-05-writing-custom-reactive-base-type.md ├── 2016-07-28-operator-concurrency-primitives-subscription-containers-2.md ├── 2017-04-21-rxjava-design-retrospect.md ├── 2016-05-13-operator-concurrency-primitives-2.md ├── 2016-08-26-schedulers-3.md ├── 2017-03-19-comparison-of-reactive-streams-part-1.md ├── 2016-09-25-the-reactive-streams-api-part-2.md ├── 2016-09-30-the-reactive-streams-api-part-3.md └── 2017-05-25-google-agera-vs-reactivex.md ├── 404.html ├── js ├── hux-blog.min.js ├── jquery.tagcloud.js ├── hux-blog.js └── anchor.min.js ├── css ├── github.min.css ├── syntax.css └── fastclick.min.js ├── feed.xml ├── index.html ├── _includes ├── head.html ├── nav.html └── footer.html ├── tags.html ├── Gruntfile.js └── _config.yml /img/404-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/404-bg.jpg -------------------------------------------------------------------------------- /img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/favicon.ico -------------------------------------------------------------------------------- /img/home-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/home-bg.jpg -------------------------------------------------------------------------------- /img/tag-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/tag-bg.jpg -------------------------------------------------------------------------------- /img/contact-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/contact-bg.jpg -------------------------------------------------------------------------------- /img/home-bg-o.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/home-bg-o.jpg -------------------------------------------------------------------------------- /img/icon_wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/icon_wechat.png -------------------------------------------------------------------------------- /img/piasy_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/piasy_avatar.jpg -------------------------------------------------------------------------------- /img/we_chat_mp_qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/img/we_chat_mp_qrcode.jpg -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | node_modules 3 | .DS_Store 4 | */.DS_Store 5 | */*/.DS_Store 6 | *.sh 7 | .jekyll-metadata 8 | .jekyll-cache 9 | -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Piasy/AdvancedRxJava/HEAD/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /less/variables.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | @brand-primary: #0085A1; 4 | @gray-dark: lighten(black, 25%); 5 | @gray: lighten(black, 50%); 6 | @gray-l: lighten(black, 75%); 7 | @white-faded: fade(white, 80%); 8 | @gray-light: #eee; 9 | -------------------------------------------------------------------------------- /archive.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Archive 4 | permalink: /archive/ 5 | --- 6 | 7 | ## {{ site.posts.size }} Blog Posts 8 | 9 | {% for post in site.posts %} 10 | * {{ post.date | date_to_string }} » [ {{ post.title }} ]({{ site.baseurl }}{{ post.url }}index.html) 11 | {% endfor %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Reactive Java 2 | 3 | The Chinese translation of blog series Advanced Reactive Java. http://akarnokd.blogspot.com/ 4 | 5 | Advanced Reactive Java http://akarnokd.blogspot.com/ 系列博客的中文翻译,已征得作者授权。该系列博客的作者是 RxJava 的核心贡献者之一。 6 | 7 | ## 协议许可 8 | 9 | [知识共享 署名-非商业性使用-相同方式共享 4.0 国际](http://creativecommons.org/licenses/by-nc-sa/4.0/index.html) 10 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | 9 | {% include nav.html %} 10 | 11 | {{ content }} 12 | 13 | {% include footer.html %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hux-blog", 3 | "title": "Hux Blog", 4 | "version": "1.0.0", 5 | "homepage": "http://huxpro.github.io", 6 | "author": "Hux", 7 | "devDependencies": { 8 | "grunt": "~0.4.5", 9 | "grunt-contrib-less": "~0.11.4", 10 | "grunt-contrib-watch": "~0.6.1", 11 | "grunt-banner": "~0.2.3", 12 | "grunt-contrib-uglify": "~0.5.1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/Huxpro/huxpro.github.io" 17 | }, 18 | "scripts": { 19 | "preview": "cd _site; python -m SimpleHTTPServer 8020", 20 | "watch": "grunt watch & npm run preview & jekyll serve -w;" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /_posts/2016-10-06-operator-internals-introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 深入理解 Operator:开篇 4 | tags: 5 | - Operator 6 | --- 7 | 8 | 原文 [Operator internals: introduction](http://akarnokd.blogspot.com/2015/10/operator-internals-introduction.html){:target="_blank"} 9 | 10 | 开发操作符通常来说都不是一个傻瓜式的活。在这个 Advanced RxJava 系列博文中,我尽可能多地涵盖了我们用到的一些基础内容,以及在开发操作符的过程中得到的经验。 11 | 12 | 但是 RxJava 有将近 150 个不同的操作符,而它们中的大多数都有些自定义的逻辑,或者说超出常规的逻辑。这些内容都是很难甚至不可能用通用的方式进行讲解的。 13 | 14 | 所以后面我会开启众多小的系列,来逐个深入分析讲解操作符,当然有些一目了然的操作符就不会讲了。不管它们实现的时候是以 `Operator` 的形式还是 `OnSubscribe` 的形式,我都称之为操作符,不同的形式是为了代码上更便捷。我将会按照字母序遍历所有的操作符,并且在同一篇文章中,顺带把所有名字不同功能一样的操作符一起进行讲解。 15 | 16 | 我会同时讲解 RxJava 1.x 和 2.x 中的操作符,这样做有两个好处,一是作为对代码实现的一次 review,另外也可以看到我们是怎么让操作符遵循 Reactive-Streams 规范的。 17 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | description: "你来到了没有知识的荒原 :(" 4 | header-img: "img/404-bg.jpg" 5 | permalink: /404.html 6 | --- 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |

404

16 | {{ page.description }} 17 | 18 |

查看历史文章

19 |
20 |
21 |
22 |
23 |
24 | 25 | 28 | -------------------------------------------------------------------------------- /js/hux-blog.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Hux Blog v1.0.0 (http://huxpro.github.io) 3 | * Copyright 2015 Hux 4 | */ 5 | 6 | $(function(){$("[data-toggle='tooltip']").tooltip()}),$(document).ready(function(){$("table").wrap("
"),$("table").addClass("table")}),$(document).ready(function(){$('iframe[src*="youtube.com"]').wrap('
'),$('iframe[src*="youtube.com"]').addClass("embed-responsive-item"),$('iframe[src*="vimeo.com"]').wrap('
'),$('iframe[src*="vimeo.com"]').addClass("embed-responsive-item")}),jQuery(document).ready(function(a){var b=1170;if(a(window).width()>b){var c=a(".navbar-custom").height();a(window).on("scroll",{previousTop:0},function(){var b=a(window).scrollTop();b0&&a(".navbar-custom").hasClass("is-fixed")?a(".navbar-custom").addClass("is-visible"):a(".navbar-custom").removeClass("is-visible is-fixed"):(a(".navbar-custom").removeClass("is-visible"),b>c&&!a(".navbar-custom").hasClass("is-fixed")&&a(".navbar-custom").addClass("is-fixed")),this.previousTop=b})}}); -------------------------------------------------------------------------------- /css/github.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;color:#333;background:#f8f8f8;-webkit-text-size-adjust:none}.hljs-comment,.diff .hljs-header{color:#998;font-style:italic}.hljs-keyword,.css .rule .hljs-keyword,.hljs-winutils,.nginx .hljs-title,.hljs-subst,.hljs-request,.hljs-status{color:#333;font-weight:bold}.hljs-number,.hljs-hexcolor,.ruby .hljs-constant{color:#008080}.hljs-string,.hljs-tag .hljs-value,.hljs-doctag,.tex .hljs-formula{color:#d14}.hljs-title,.hljs-id,.scss .hljs-preprocessor{color:#900;font-weight:bold}.hljs-list .hljs-keyword,.hljs-subst{font-weight:normal}.hljs-class .hljs-title,.hljs-type,.vhdl .hljs-literal,.tex .hljs-command{color:#458;font-weight:bold}.hljs-tag,.hljs-tag .hljs-title,.hljs-rule .hljs-property,.django .hljs-tag .hljs-keyword{color:#000080;font-weight:normal}.hljs-attribute,.hljs-variable,.lisp .hljs-body,.hljs-name{color:#008080}.hljs-regexp{color:#009926}.hljs-symbol,.ruby .hljs-symbol .hljs-string,.lisp .hljs-keyword,.clojure .hljs-keyword,.scheme .hljs-keyword,.tex .hljs-special,.hljs-prompt{color:#990073}.hljs-built_in{color:#0086b3}.hljs-preprocessor,.hljs-pragma,.hljs-pi,.hljs-doctype,.hljs-shebang,.hljs-cdata{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.diff .hljs-change{background:#0086b3}.hljs-chunk{color:#aaa} -------------------------------------------------------------------------------- /feed.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: null 3 | --- 4 | 5 | 6 | 7 | {{ site.title | xml_escape }} 8 | {{ site.description | xml_escape }} 9 | {{ site.url }}{{ site.baseurl }}/ 10 | 11 | {{ site.time | date_to_rfc822 }} 12 | {{ site.time | date_to_rfc822 }} 13 | Jekyll v{{ jekyll.version }} 14 | {% for post in site.posts limit:10 %} 15 | 16 | {{ post.title | xml_escape }} 17 | 为避免被弱智的爬虫爬取,feed 只包含摘要,完整内容请查看原文 {{ post.url | prepend: site.baseurl | prepend: site.url }}
{{ post.content | strip_html | truncate:200 }}
18 | {{ post.date | date_to_rfc822 }} 19 | {% for tag in post.tags %} 20 | {{ tag | xml_escape }} 21 | {% endfor %} 22 | {% for cat in post.categories %} 23 | {{ cat | xml_escape }} 24 | {% endfor %} 25 |
26 | {% endfor %} 27 |
28 |
29 | -------------------------------------------------------------------------------- /less/sidebar.less: -------------------------------------------------------------------------------- 1 | @import "variables.less"; 2 | 3 | // Sidebar Components 4 | 5 | // Large Screen 6 | @media (min-width: 1200px){ 7 | .post-container, .sidebar-container{ 8 | padding-right: 5%; 9 | } 10 | } 11 | @media (min-width: 768px){ 12 | .post-container{ 13 | padding-right: 5%; 14 | } 15 | } 16 | 17 | // Container of Sidebar, also Friends 18 | .sidebar-container{ 19 | color: @gray-l; 20 | font-size: 14px; 21 | h5{ 22 | color: @gray; 23 | padding-bottom: 1em; 24 | a{ 25 | color: @gray !important; 26 | text-decoration: none; 27 | } 28 | } 29 | a{ 30 | color: @gray-l !important; 31 | &:hover, &:active{ 32 | color: @brand-primary !important; 33 | } 34 | } 35 | .tags{ 36 | a{ 37 | border-color: @gray-l; 38 | &:hover, &:active{ 39 | border-color: @brand-primary; 40 | } 41 | } 42 | } 43 | .short-about{ 44 | img{ 45 | width: 80%; 46 | display: block; 47 | border-radius: 5px; 48 | margin-bottom: 20px; 49 | } 50 | p{ 51 | margin-top: 0px; 52 | margin-bottom: 20px; 53 | } 54 | .list-inline>li{ 55 | padding-left: 0px; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | description: "NEVER STOP" 4 | --- 5 | 6 | {% for post in paginator.posts %} 7 |
8 | 9 |

10 | {{ post.title }} 11 |

12 | {% if post.subtitle %} 13 |

14 | {{ post.subtitle }} 15 |

16 | {% endif %} 17 |
18 | {{ post.content | strip_html | truncate:200 }} 19 |
20 |
21 | 24 |
25 |
26 | {% endfor %} 27 | 28 | 29 | {% if paginator.total_pages > 1 %} 30 | 42 | {% endif %} 43 | -------------------------------------------------------------------------------- /_posts/2017-06-18-the-reactive-scrabble-benchmarks.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 响应式编程库 Scrabble 基准测试 4 | tags: 5 | - 对比点评 6 | --- 7 | 8 | 原文 [The Reactive Scrabble benchmarks](http://akarnokd.blogspot.com/2016/12/the-reactive-scrabble-benchmarks.html){:target="_blank"}。 9 | 10 | ## 介绍 11 | 12 | 过去一年多的时间里,我以神秘的莎士比亚 Scrabble 之名发布了很多基准测试结果。在本文中,我将解释这个基准测试是什么、从何而来、如何运作、有何目的,以及如何在未测试的库上面运行这一基准测试。 13 | 14 | ## 历史 15 | 16 | 这一基准测试由 [Jose Paumard 设计开发](https://github.com/JosePaumard/jdk8-stream-rx-comparison),测试结果首次于[他的 2015 Devoxx 演讲](https://www.youtube.com/watch?v=fabN6HNZ2qY)上发布。这一基准用于测量某个数据处理的库能以多快的速度从一个单词集合中找到最有价值的单词,这些单词取自莎士比亚的剧本,单词评分则基于 Scrabble 框架定义的规则。彼时参与测评的是 RxJava 1.0.x 版本,令我吃惊的是,相比于 Java 8 Stream API,RxJava 的性能实在差得离谱: 17 | 18 | ![](https://imgs.piasy.com/2017-06-18-jose_scrabble_results.png) 19 | 20 | 这一基准测试利用了 JMH,完全同步运行;在没有任何线程弹跳的情况下 RxJava 运行耗时长 10 倍,或者更可能的是,它使用的一系列操作符有 10 倍的开销。此外,Jose 还增加了一个并行序列的版本,在 join 线程之前,主循环并行执行。 21 | 22 | 更令人失望的是,RxJava 2 开发者预览版的表现也一样糟糕([二月份在一个相对弱的 CPU 上运行的测试](https://twitter.com/akarnokd/status/696291409209487360))。 23 | 24 | 因此,我并没有埋怨这个基准测试或者其作者,而是尝试理解这一基准测试期望的性能点,并据此优化 RxJava 2 的性能,如有可能还要移植到 RxJava 1.x 上去。 25 | 26 | _译者注:描述 benchmark 代码的部分冗长无味,我就不翻译了。_ 27 | 28 | ## 最初测试 Stream API 的基准测试 29 | 30 | …… 31 | 32 | ## 最初测试 RxJava 的基准测试 33 | 34 | …… 35 | 36 | ## 优化的测试 RxJava 基准测试 37 | 38 | …… 39 | 40 | ## 测试其他库 41 | 42 | 过去一年我们评测了各种库,结果如下图: 43 | 44 | ![](https://imgs.piasy.com/2017-06-18-Scrabble_12_14.jpg) 45 | 46 | _译者注:此处省略各个库测试过程中的一些注意事项。_ 47 | 48 | ## 总结 49 | 50 | 编写一个响应式编程库很难,为这些库编写基准测试也不简单。要理解为何不同库之间快慢差异之巨,需要对同步异步的设计开发都有深厚的功底。 51 | 52 | 不像一年前我对 Scrabble 基准测试集恼羞成怒,过去一年里我花了很多精力去改进和优化我能影响到的库,多亏了这些不懈的努力,这些库的架构和概念上都有所改进,它们在这个基准测试集上以及通用场景下都表现更佳了。 53 | 54 | 我必须警告读者,不要把 Scrabble 基准测试集的结果作为这些库排名的终极指标。它们在这个基准测试集上表现如此,并不代表在其他场景或任务下也表现如此。计算量/IO 的开销也许就会掩盖这一丁点的差异。 55 | -------------------------------------------------------------------------------- /less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | 3 | .transition-all() { 4 | -webkit-transition: all 0.5s; 5 | -moz-transition: all 0.5s; 6 | transition: all 0.5s; 7 | } 8 | 9 | .background-cover() { 10 | -webkit-background-size: cover; 11 | -moz-background-size: cover; 12 | background-size: cover; 13 | -o-background-size: cover; 14 | } 15 | 16 | .serif() { 17 | font-family: 'Lora', 'Times New Roman', serif; 18 | } 19 | 20 | .sans-serif () { 21 | /* Hux learn from 22 | * TypeIsBeautiful, 23 | * [This Post](http://zhuanlan.zhihu.com/ibuick/20186806) etc. 24 | */ 25 | font-family: 26 | // System Font // https://www.webkit.org/blog/3709/using-the-system-font-in-web-content/ 27 | -apple-system, // OSX ^10.11 & iOS ^9 San Francisco & 苹方 28 | 29 | // English First 30 | "Helvetica Neue", // OSX 31 | "Arial", // Win "Helvetica" 32 | //" Segoe UI", // Win ^8 33 | 34 | // Chinese Fallback 35 | "PingFang SC", // OSX ^10.11 & iOS ^9 苹方(华康信凭黑) 36 | "Hiragino Sans GB", // OSX ^10.6 冬青黑体 37 | "STHeiti", // OSX <10.6 & iOS <9 华文黑体 38 | "Microsoft YaHei", // Win 微软雅黑 39 | "Microsoft JhengHei", // Win 微软正黑 40 | "Source Han Sans SC", // SourceHan - begin 思源黑体 41 | "Noto Sans CJK SC", 42 | "Source Han Sans CN", 43 | "Noto Sans SC", 44 | "Source Han Sans TC", 45 | "Noto Sans CJK TC", // SourceHan - end 46 | "WenQuanYi Micro Hei", // Linux 文泉驿微米黑 47 | SimSun, // Win old 中易宋体 48 | sans-serif; // System Fallback 49 | 50 | line-height: 1.7; 51 | } 52 | -------------------------------------------------------------------------------- /_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% if page.title %}{{ page.title }} - {{ site.SEOTitle }}{% else %}{{ site.SEOTitle }}{% endif %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tags.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tags 3 | layout: default 4 | description: keep hungry keep foolish 5 | header-img: "img/tag-bg.jpg" 6 | --- 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 |

{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}

15 | {{ page.description }} 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 |
28 | {% for tag in site.tags %} 29 | {{ tag[0] }} 30 | {% endfor %} 31 |
32 | 33 | 34 | {% for tag in site.tags %} 35 |
36 | 37 | {{ tag[0] }} 38 | 39 | {% for post in tag[1] %} 40 | 44 |
45 | 46 |

47 | {{ post.title }} 48 |

49 | {% if post.subtitle %} 50 |

51 | {{ post.subtitle }} 52 |

53 | {% endif %} 54 |
55 | 56 |
57 |
58 | {% endfor %} 59 |
60 | {% endfor %} 61 | 62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /_posts/2017-02-24-operator-internals-autoconnect.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 深入理解 Operator:AutoConnect 4 | tags: 5 | - Operator 6 | --- 7 | 8 | 原文 [Operator internals: AutoConnect](http://akarnokd.blogspot.com/2015/10/operator-internals-autoconnect.html){:target="_blank"}。 9 | 10 | ## 介绍 11 | 12 | `autoConnect` 操作符是 `ConnectableObservable` 类的一部分,它允许我们在一定数量的 Subscriber 订阅(返回的 Observable)之后,自动连接原来的 ConnectableObservable。这个操作符返回的就是一个普通的 Observable,因此我们可以更容易地和其他操作符串联起来。 13 | 14 | 设计这一操作符有两个初衷。首先,我们有时候希望只有当一定数量的 Subscriber 订阅之后才连接 ConnectableObservable,没有 autoConnect 我们很难实现这一功能;其次,另一个 cache 操作符不支持高级的留存策略控制,例如按时/按量控制缓存的数据量,而实现像 cache 这样首次订阅触发连接的逻辑又比较繁琐(_所以干脆实现一个新的操作符,同时具备留存控制和自动触发功能_)。 15 | 16 | 总的实现思路很简单,我们用一个 AtomicInteger 在 OnSubscribe 类中记录抵达的 Subscriber 数量,一旦到达目标数量,我们就可以调用 connect 了。 17 | 18 | 但这里有一个细节比较复杂:ConnectableObservable 同步取消订阅的支持。如果使用上面实现的 autoConnect,我们就无法取消订阅上游数据流了,这和 cache 的行为很相似。解决办法就是准备一个回调,把它传给 connect 函数。 19 | 20 | ## 实现细节 21 | 22 | 由于这个操作符不需要请求管理,因此 1.x 和 2.x 的实现基本一样: 23 | 24 | ~~~ java 25 | public Observable autoConnect(int numConnections, 26 | Action1 connection) { // (1) 27 | if (numConnection == 0) { 28 | connect(connection); // (2) 29 | return this; 30 | } 31 | AtomicInteger count = new AtomicInteger(); 32 | return create(s -> { 33 | unsafeSubscribe(s); // (3) 34 | if (count.incrementAndGet() == numConnections) { // (4) 35 | connect(connection); 36 | } 37 | }); 38 | } 39 | ~~~ 40 | 41 | 有几点还是值得一提的: 42 | 43 | 1. RxJava 还有两个重载版本,一个不需要 `numConnections` 参数,默认为 1,另一个不需要 `connection` 参数,默认什么也不做; 44 | 2. 如果 numConnections 为 0,我们直接连接 ConnectableObservable,这种情况下我们不需要做任何包装,可以直接返回 this; 45 | 3. 如果 numConnections 不为 0,那我们就记录接下来到达的 Subscriber,并在计数到达预定数量之和,连接 ConnectableObservable;但在检查计数之前,我们先把 Subscriber 订阅到 ConnectableObservable 上;先订阅这一点很重要,因为如果 numConnections 为 1,那连接操作可能会导致数据的发射,而如果后订阅,那实际的 Subscriber 就收不到数据了; 46 | 4. 一旦 Subscriber 计数到达预定数量,我们就连接 ConnectableObservable,这会导致触发最初提供的回调; 47 | 48 | 此时你可能会想,如果 numConnections 为 2,第一个 Subscriber 订阅之后立即取消订阅,那第二个 Subscriber 是否应该触发连接?这取决于我们的需求。autoConnect 显然没有处理这一问题(_也就是说会触发连接_),在取消订阅时调用 decrementAndGet() 就可以处理这一问题。 49 | 50 | 这么处理的原因是 autoConnect 最初是用来在特定情况下替代 refCount() 和 share() 的。 51 | 52 | ## 总结 53 | 54 | autoConnect 可以说是前 10% 简单的操作符了,所以它的特性和实现都很简洁明了。 55 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | uglify: { 7 | main: { 8 | src: 'js/<%= pkg.name %>.js', 9 | dest: 'js/<%= pkg.name %>.min.js' 10 | } 11 | }, 12 | less: { 13 | expanded: { 14 | options: { 15 | paths: ["css"] 16 | }, 17 | files: { 18 | "css/<%= pkg.name %>.css": "less/<%= pkg.name %>.less" 19 | } 20 | }, 21 | minified: { 22 | options: { 23 | paths: ["css"], 24 | cleancss: true 25 | }, 26 | files: { 27 | "css/<%= pkg.name %>.min.css": "less/<%= pkg.name %>.less" 28 | } 29 | } 30 | }, 31 | banner: '/*!\n' + 32 | ' * <%= pkg.title %> v<%= pkg.version %> (<%= pkg.homepage %>)\n' + 33 | ' * Copyright <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' + 34 | ' */\n', 35 | usebanner: { 36 | dist: { 37 | options: { 38 | position: 'top', 39 | banner: '<%= banner %>' 40 | }, 41 | files: { 42 | src: ['css/<%= pkg.name %>.css', 'css/<%= pkg.name %>.min.css', 'js/<%= pkg.name %>.min.js'] 43 | } 44 | } 45 | }, 46 | watch: { 47 | scripts: { 48 | files: ['js/<%= pkg.name %>.js'], 49 | tasks: ['uglify'], 50 | options: { 51 | spawn: false, 52 | }, 53 | }, 54 | less: { 55 | files: ['less/*.less'], 56 | tasks: ['less'], 57 | options: { 58 | spawn: false, 59 | } 60 | }, 61 | }, 62 | }); 63 | 64 | // Load the plugins. 65 | grunt.loadNpmTasks('grunt-contrib-uglify'); 66 | grunt.loadNpmTasks('grunt-contrib-less'); 67 | grunt.loadNpmTasks('grunt-banner'); 68 | grunt.loadNpmTasks('grunt-contrib-watch'); 69 | 70 | // Default task(s). 71 | grunt.registerTask('default', ['uglify', 'less', 'usebanner']); 72 | 73 | }; 74 | -------------------------------------------------------------------------------- /js/jquery.tagcloud.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | $.fn.tagcloud = function(options) { 4 | var opts = $.extend({}, $.fn.tagcloud.defaults, options); 5 | tagWeights = this.map(function(){ 6 | return $(this).attr("rel"); 7 | }); 8 | tagWeights = jQuery.makeArray(tagWeights).sort(compareWeights); 9 | lowest = tagWeights[0]; 10 | highest = tagWeights.pop(); 11 | range = highest - lowest; 12 | if(range === 0) {range = 1;} 13 | // Sizes 14 | if (opts.size) { 15 | fontIncr = (opts.size.end - opts.size.start)/range; 16 | } 17 | // Colors 18 | if (opts.color) { 19 | colorIncr = colorIncrement (opts.color, range); 20 | } 21 | return this.each(function() { 22 | weighting = $(this).attr("rel") - lowest; 23 | if (opts.size) { 24 | $(this).css({"font-size": opts.size.start + (weighting * fontIncr) + opts.size.unit}); 25 | } 26 | if (opts.color) { 27 | // change color to background-color 28 | $(this).css({"backgroundColor": tagColor(opts.color, colorIncr, weighting)}); 29 | } 30 | }); 31 | }; 32 | 33 | $.fn.tagcloud.defaults = { 34 | size: {start: 14, end: 18, unit: "pt"} 35 | }; 36 | 37 | // Converts hex to an RGB array 38 | function toRGB (code) { 39 | if (code.length == 4) { 40 | code = jQuery.map(/\w+/.exec(code), function(el) {return el + el; }).join(""); 41 | } 42 | hex = /(\w{2})(\w{2})(\w{2})/.exec(code); 43 | return [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; 44 | } 45 | 46 | // Converts an RGB array to hex 47 | function toHex (ary) { 48 | return "#" + jQuery.map(ary, function(i) { 49 | hex = i.toString(16); 50 | hex = (hex.length == 1) ? "0" + hex : hex; 51 | return hex; 52 | }).join(""); 53 | } 54 | 55 | function colorIncrement (color, range) { 56 | return jQuery.map(toRGB(color.end), function(n, i) { 57 | return (n - toRGB(color.start)[i])/range; 58 | }); 59 | } 60 | 61 | function tagColor (color, increment, weighting) { 62 | rgb = jQuery.map(toRGB(color.start), function(n, i) { 63 | ref = Math.round(n + (increment[i] * weighting)); 64 | if (ref > 255) { 65 | ref = 255; 66 | } else { 67 | if (ref < 0) { 68 | ref = 0; 69 | } 70 | } 71 | return ref; 72 | }); 73 | return toHex(rgb); 74 | } 75 | 76 | function compareWeights(a, b) 77 | { 78 | return a - b; 79 | } 80 | 81 | })(jQuery); 82 | -------------------------------------------------------------------------------- /js/hux-blog.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Clean Blog v1.0.0 (http://startbootstrap.com) 3 | * Copyright 2015 Start Bootstrap 4 | * Licensed under Apache 2.0 (https://github.com/IronSummitMedia/startbootstrap/blob/gh-pages/LICENSE) 5 | */ 6 | 7 | // Tooltip Init 8 | $(function() { 9 | $("[data-toggle='tooltip']").tooltip(); 10 | }); 11 | 12 | 13 | // make all images responsive 14 | /* 15 | * Unuse by Hux 16 | * actually only Portfolio-Pages can't use it and only post-img need it. 17 | * so I modify the _layout/post and CSS to make post-img responsive! 18 | */ 19 | // $(function() { 20 | // $("img").addClass("img-responsive"); 21 | // }); 22 | 23 | // responsive tables 24 | $(document).ready(function() { 25 | $("table").wrap("
"); 26 | $("table").addClass("table"); 27 | }); 28 | 29 | // responsive embed videos 30 | $(document).ready(function () { 31 | $('iframe[src*="youtube.com"]').wrap('
'); 32 | $('iframe[src*="youtube.com"]').addClass('embed-responsive-item'); 33 | $('iframe[src*="vimeo.com"]').wrap('
'); 34 | $('iframe[src*="vimeo.com"]').addClass('embed-responsive-item'); 35 | }); 36 | 37 | // Navigation Scripts to Show Header on Scroll-Up 38 | jQuery(document).ready(function($) { 39 | var MQL = 1170; 40 | 41 | //primary navigation slide-in effect 42 | if ($(window).width() > MQL) { 43 | var headerHeight = $('.navbar-custom').height(); 44 | $(window).on('scroll', { 45 | previousTop: 0 46 | }, 47 | function() { 48 | var currentTop = $(window).scrollTop(); 49 | //check if user is scrolling up 50 | if (currentTop < this.previousTop) { 51 | //if scrolling up... 52 | if (currentTop > 0 && $('.navbar-custom').hasClass('is-fixed')) { 53 | $('.navbar-custom').addClass('is-visible'); 54 | } else { 55 | $('.navbar-custom').removeClass('is-visible is-fixed'); 56 | } 57 | } else { 58 | //if scrolling down... 59 | $('.navbar-custom').removeClass('is-visible'); 60 | if (currentTop > headerHeight && !$('.navbar-custom').hasClass('is-fixed')) $('.navbar-custom').addClass('is-fixed'); 61 | } 62 | this.previousTop = currentTop; 63 | }); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /_posts/2016-10-16-operator-internals-all.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 深入理解 Operator:All,Any 和 Exists 4 | tags: 5 | - Operator 6 | --- 7 | 8 | 原文 [Operator internals: All, Any, Exists](http://akarnokd.blogspot.com/2015/10/operator-internals-all.html){:target="_blank"} 9 | 10 | ## 介绍 11 | 12 | `all` 这个操作符,会检查上游发出的所有数据是否都满足给定的条件(`predicate`),如果有任何一个数据不满足条件,就立即发出 `false` 然后结束,如果所有数据都满足条件(或者没有数据),就会发出 `true` 后结束。而 `any` 则和 `all` 在逻辑上相反,只要有一个数据满足条件,就立即发出 `true` 然后结束,如果所有数据都不满足条件(或者没有数据),就会发出 `false` 后结束。这两个操作符都满足 backpressure 的要求。 13 | 14 | 我们需要了解这两个操作符的以下特性/要求: 15 | 16 | + 由于发往下游的数据只有一个,所以我们不需要考虑如何向上游请求数据,直接向上游请求无限(`Long.MAX_VALUE`)个数据即可。这样做的好处是可能触发上游的快路径,进而减小运行开销。 17 | + 同样由于发往下游的数据只有一个,即便上游没有数据也会有一个数据发往下游,我们需要考虑来自下游的请求,只有在下游请求过之后才发出这个唯一的数据。 18 | 19 | ## 1.x 的实现 20 | 21 | 1.x 的实现非常直观,它向上游请求 `Long.MAX_VALUE` 个数据,利用 `SingleDelayedProducer` 来延迟对数据的发射,只有当下游请求了之后才发出。 22 | 23 | 由于 backpressure 在 1.x 中是可选的,所以我们无法在 `onNext` 中遇到不满足条件的数据之后立即发出 `false`,因为只有 `SingleDelayedProducer` 知道当前是否已经有了来自下游的请求(_我们要遵循 backpressure,但上游不一定遵循了,我们必须可靠地遵循 backpressure,所以我们必须经过 SingleDelayedProducer 中转_)。 24 | 25 | ## 2.x 的实现 26 | 27 | 2.x 的实现更长一些,因为我直接把 SingleDelayedProducer 的逻辑实现在了这个操作符中,这样可以减少内存分配。 28 | 29 | backpressure 的要求没有变,但由于 `onSubscribe` 的调用是必须的,所以 `onNext` 到来时就说明下游一定已经请求过了数据。 30 | 31 | 如果在 `onNext` 中遇到了不满足条件的数据,就无需进行中转了,我们可以直接发往下游,因为上游发来了数据,就说明下游一定已经有了请求。但对于空的上游来说,我们还是需要进行中转的。这里的状态机和[前文](/AdvancedRxJava/2016/06/04/operator-concurrency-primitives-4/index.html){:target="_blank"}中的状态机很类似。有一个值得注意的区别就在于,由于我们知道需要延迟发射的数据一定是 `true`,所以我们无需一个变量保存它的值了。 32 | 33 | 让我们看看 `AllSubscriber#onNext()`: 34 | 35 | ~~~ java 36 | @Override 37 | public void onNext(T t) { 38 | if (done) { // (1) 39 | return; 40 | } 41 | boolean b; 42 | try { 43 | b = predicate.test(t); 44 | } catch (Throwable e) { // (2) 45 | lazySet(HAS_REQUEST_HAS_VALUE); 46 | done = true; 47 | s.cancel(); 48 | actual.onError(e); 49 | return; 50 | } 51 | if (!b) { 52 | lazySet(HAS_REQUEST_HAS_VALUE); // (3) 53 | done = true; 54 | s.cancel(); 55 | actual.onNext(false); 56 | actual.onComplete(); 57 | } 58 | } 59 | ~~~ 60 | 61 | 1. 取消订阅在 Reactive-Streams 和 RxJava 1.x 中都只是尽力而为,我们的逻辑不能完全依赖取消订阅。利用 `done` 标记,我们会在结束/取消之后,丢弃所有数据。 62 | 2. 检查数据的代码可能出错,出错之后我们就把状态设置为终结状态 `HAS_REQUEST_HAS_VALUE`,这会让 `request()` 函数不请求新的数据。同时我们设置 done 标记,并且取消上游。 63 | 3. 如果数据不满足条件,我们可以直接向下游发出 false,并且取消上游。同样,状态机也会进入终结状态。 64 | 65 | ## 总结 66 | 67 | `all` 和 `any` 操作符很简单,我觉得按从简单到复杂排名可以进入前 20%,但如果上游是空的,天真的实现方式可能会违背 backpressure 的要求,发出下游没有请求过的数据,因此我们需要 `SingleDelayedProducer` 来填补这个空缺。 68 | -------------------------------------------------------------------------------- /_includes/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 49 | 77 | -------------------------------------------------------------------------------- /css/syntax.css: -------------------------------------------------------------------------------- 1 | /* to make lines scroll instead of wrap */ 2 | /* from http://stackoverflow.com/a/23393920 */ 3 | 4 | .highlight pre code * { 5 | white-space: nowrap; // this sets all children inside to nowrap 6 | } 7 | 8 | .highlight pre { 9 | overflow-x: auto; // this sets the scrolling in x 10 | } 11 | 12 | .highlight pre code { 13 | white-space: pre; // forces to respect
 formatting
14 | }
15 | 
16 | 
17 | /*
18 |  * GitHub style for Pygments syntax highlighter, for use with Jekyll
19 |  * Courtesy of GitHub.com
20 |  */
21 | 
22 | .highlight pre, pre, .highlight .hll { background-color: #f8f8f8; border: 1px solid #ccc; padding: 6px 10px; border-radius: 3px; }
23 | .highlight .c { color: #999988; font-style: italic; }
24 | .highlight .err { color: #a61717; background-color: #e3d2d2; }
25 | .highlight .k { font-weight: bold; }
26 | .highlight .o { font-weight: bold; }
27 | .highlight .cm { color: #999988; font-style: italic; }
28 | .highlight .cp { color: #999999; font-weight: bold; }
29 | .highlight .c1 { color: #999988; font-style: italic; }
30 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic; }
31 | .highlight .gd { color: #000000; background-color: #ffdddd; }
32 | .highlight .gd .x { color: #000000; background-color: #ffaaaa; }
33 | .highlight .ge { font-style: italic; }
34 | .highlight .gr { color: #aa0000; }
35 | .highlight .gh { color: #999999; }
36 | .highlight .gi { color: #000000; background-color: #ddffdd; }
37 | .highlight .gi .x { color: #000000; background-color: #aaffaa; }
38 | .highlight .go { color: #888888; }
39 | .highlight .gp { color: #555555; }
40 | .highlight .gs { font-weight: bold; }
41 | .highlight .gu { color: #800080; font-weight: bold; }
42 | .highlight .gt { color: #aa0000; }
43 | .highlight .kc { font-weight: bold; }
44 | .highlight .kd { font-weight: bold; }
45 | .highlight .kn { font-weight: bold; }
46 | .highlight .kp { font-weight: bold; }
47 | .highlight .kr { font-weight: bold; }
48 | .highlight .kt { color: #445588; font-weight: bold; }
49 | .highlight .m { color: #009999; }
50 | .highlight .s { color: #dd1144; }
51 | .highlight .n { color: #333333; }
52 | .highlight .na { color: teal; }
53 | .highlight .nb { color: #0086b3; }
54 | .highlight .nc { color: #445588; font-weight: bold; }
55 | .highlight .no { color: teal; }
56 | .highlight .ni { color: purple; }
57 | .highlight .ne { color: #990000; font-weight: bold; }
58 | .highlight .nf { color: #990000; font-weight: bold; }
59 | .highlight .nn { color: #555555; }
60 | .highlight .nt { color: navy; }
61 | .highlight .nv { color: teal; }
62 | .highlight .ow { font-weight: bold; }
63 | .highlight .w { color: #bbbbbb; }
64 | .highlight .mf { color: #009999; }
65 | .highlight .mh { color: #009999; }
66 | .highlight .mi { color: #009999; }
67 | .highlight .mo { color: #009999; }
68 | .highlight .sb { color: #dd1144; }
69 | .highlight .sc { color: #dd1144; }
70 | .highlight .sd { color: #dd1144; }
71 | .highlight .s2 { color: #dd1144; }
72 | .highlight .se { color: #dd1144; }
73 | .highlight .sh { color: #dd1144; }
74 | .highlight .si { color: #dd1144; }
75 | .highlight .sx { color: #dd1144; }
76 | .highlight .sr { color: #009926; }
77 | .highlight .s1 { color: #dd1144; }
78 | .highlight .ss { color: #990073; }
79 | .highlight .bp { color: #999999; }
80 | .highlight .vc { color: teal; }
81 | .highlight .vg { color: teal; }
82 | .highlight .vi { color: teal; }
83 | .highlight .il { color: #009999; }
84 | .highlight .gc { color: #999; background-color: #EAF2F5; }
85 | 


--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
  1 | # Site settings
  2 | title: Advanced RxJava
  3 | SEOTitle: Piasy的博客 | Piasy Blog
  4 | header-img: img/home-bg.jpg
  5 | email: xz4215@gmail.com
  6 | description: "NEVER STOP"
  7 | keyword: "Piasy"
  8 | url: "https://blog.piasy.com"              # your host, for absolute URL
  9 | baseurl: "/AdvancedRxJava"                             # for example, '/blog' if your blog hosted on 'host/blog'
 10 | 
 11 | 
 12 | 
 13 | # SNS settings
 14 | RSS: true
 15 | footer-links:
 16 |     email: xz4215@gmail.com
 17 |     github:    Piasy
 18 |     linkedin: piasy
 19 |     stackoverflow: users/3077508/piasy
 20 |     googleplus: +許建林
 21 | 
 22 | 
 23 | 
 24 | # Build settings
 25 | highlighter: rouge
 26 | permalink: pretty
 27 | paginate: 10
 28 | exclude: ["less","node_modules","Gruntfile.js","package.json","README.md", "_drafts", "raw"]
 29 | anchorjs: true                          # if you want to customize anchor. check out line:181 of `post.html`
 30 | 
 31 | 
 32 | 
 33 | # Markdown settings
 34 | # replace redcarpet to kramdown,
 35 | # although redcarpet can auto highlight code, the lack of header-id make the catalog impossible, so I switch to kramdown
 36 | # document: http://jekyllrb.com/docs/configuration/#kramdown
 37 | markdown: kramdown
 38 | kramdown:
 39 |   input: GFM                            # use Github Flavored Markdown !important
 40 | 
 41 | 
 42 | 
 43 | # Disqus settings
 44 | disqus_username: piasy
 45 | 
 46 | # Duoshuo settings
 47 | #duoshuo_username: huxblog
 48 | # Share component is depend on Comment so we can NOT use share only.
 49 | #duoshuo_share: true                     # set to false if you want to use Comment without Sharing
 50 | 
 51 | 
 52 | plugins:
 53 |   - jekyll-sitemap # Create a sitemap using the official Jekyll sitemap gem
 54 |   - jekyll-paginate
 55 | 
 56 | 
 57 | # Analytics settings
 58 | # Baidu Analytics
 59 | ba_track_id: 0047a0e87acd4fe7ce36981131e63eca
 60 | # Google Analytics
 61 | ga_track_id: 'UA-68342301-1'            # Format: UA-xxxxxx-xx
 62 | ga_domain: blog.piasy.com
 63 | 
 64 | 
 65 | 
 66 | # Sidebar settings
 67 | sidebar: true                           # whether or not using Sidebar.
 68 | sidebar-about-description: "NEVER STOP"
 69 | sidebar-avatar: ../img/we_chat_mp_qrcode.jpg      # use absolute URL, seeing it's used in both `/` and `/about/`
 70 | 
 71 | 
 72 | 
 73 | # Featured Tags
 74 | featured-tags: true                     # whether or not using Feature-Tags
 75 | featured-condition-size: 0              # A tag will be featured if the size of it is more than this condition value
 76 | 
 77 | 
 78 | 
 79 | # Friends
 80 | friends: [
 81 |     {
 82 |         title: "DiyCode",
 83 |         href: "http://www.diycode.cc/"
 84 |     },
 85 |     {
 86 |         title: "CodeKK",
 87 |         href: "http://codekk.com"
 88 |     },
 89 |     {
 90 |         title: "Trinea",
 91 |         href: "http://www.trinea.cn/"
 92 |     },
 93 |     {
 94 |         title: "Stay",
 95 |         href: "http://notes.stay4it.com/"
 96 |     },
 97 |     {
 98 |         title: "技术小黑屋",
 99 |         href: "http://droidyue.com/"
100 |     },
101 |     {
102 |         title: "安卓开发者官博",
103 |         href: "http://android-developers.blogspot.com/"
104 |     },
105 |     {
106 |         title: "老罗的Android之旅",
107 |         href: "http://blog.csdn.net/Luoshengyang"
108 |     },
109 |     {
110 |         title: "胡凯",
111 |         href: "http://hukai.me/"
112 |     },
113 |     {
114 |         title: "代码家",
115 |         href: "http://blog.daimajia.com/"
116 |     },
117 |     {
118 |         title: "郭霖的专栏",
119 |         href: "http://blog.csdn.net/guolin_blog"
120 |     },
121 |     {
122 |         title: "stormzhang博客精华",
123 |         href: "http://stormzhang.com/"
124 |     },
125 |     {
126 |         title: "张鸿洋",
127 |         href: "http://blog.csdn.net/lmj623565791/"
128 |     },
129 |     {
130 |         title: "张兴业的博客",
131 |         href: "http://blog.csdn.net/xyz_lmn"
132 |     },
133 |     {
134 |         title: "hi大头鬼hi",
135 |         href: "http://blog.csdn.net/lzyzsd/"
136 |     }
137 | ]
138 | 


--------------------------------------------------------------------------------
/js/anchor.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 |  * AnchorJS - v1.1.1 - 2015-05-23
3 |  * https://github.com/bryanbraun/anchorjs
4 |  * Copyright (c) 2015 Bryan Braun; Licensed MIT
5 |  */
6 | function AnchorJS(A){"use strict";this.options=A||{},this._applyRemainingDefaultOptions=function(A){this.options.icon=this.options.hasOwnProperty("icon")?A.icon:"",this.options.visible=this.options.hasOwnProperty("visible")?A.visible:"hover",this.options.placement=this.options.hasOwnProperty("placement")?A.placement:"right",this.options.class=this.options.hasOwnProperty("class")?A.class:""},this._applyRemainingDefaultOptions(A),this.add=function(A){var e,t,o,n,i,s,a,l,c,r,h,g,B,Q;if(this._applyRemainingDefaultOptions(this.options),A){if("string"!=typeof A)throw new Error("The selector provided to AnchorJS was invalid.")}else A="h1, h2, h3, h4, h5, h6";if(e=document.querySelectorAll(A),0===e.length)return!1;for(this._addBaselineStyles(),t=document.querySelectorAll("[id]"),o=[].map.call(t,function(A){return A.id}),i=0;i',B=document.createElement("div"),B.innerHTML=g,Q=B.childNodes,"always"===this.options.visible&&(Q[0].style.opacity="1"),""===this.options.icon&&(Q[0].style.fontFamily="anchorjs-icons",Q[0].style.fontStyle="normal",Q[0].style.fontVariant="normal",Q[0].style.fontWeight="normal"),"left"===this.options.placement?(Q[0].style.position="absolute",Q[0].style.marginLeft="-1em",Q[0].style.paddingRight="0.5em",e[i].insertBefore(Q[0],e[i].firstChild)):(Q[0].style.paddingLeft="0.375em",e[i].appendChild(Q[0]))}return this},this.remove=function(A){for(var e,t=document.querySelectorAll(A),o=0;o 
 49 | extends AtomicReference
 50 | implements Subscriber, Subscription {
 51 |     volatile long missedRequested;
 52 |     static final AtomicLongFieldUpdater MISSED_REQUESTED = ...;
 53 |  
 54 |     static final Subscription CANCELLED = ...;
 55 | }
 56 | ~~~
 57 | 
 58 | 它当然实现了 `Subscriber`,为了方便,也实现了 `Subscription`(这样我们就有 `request()` 和 `cancel()` 了)。它有一个 Subscription 常量 `CANCELLED`,用来表示已被取消订阅,并且告知迟来的 `request()` 和 `cancel()` 不要做任何事了。通过继承自 `AtomicReference`,这样我们就可以利用原子性接口保存和访问将要到来的 Subscription 了(_译者注:这里违背了“组合优于继承”原则,但带来了性能提升_)。
 59 | 
 60 | 让我们首先看看 `cancel()` 的实现:
 61 | 
 62 | ~~~ java
 63 | @Override
 64 | public void cancel() {
 65 |     Subscription s = get();
 66 |     if (s != CANCELLED) {
 67 |         s = getAndSet(CANCELLED);
 68 |         if (s != CANCELLED && s != null) {
 69 |             s.cancel();
 70 |         }
 71 |     }
 72 | }
 73 | ~~~
 74 | 
 75 | 现在这段代码看起来应该很熟悉了。当 `cancel()` 被调用时,如果当前的 `Subscription` 不是 CANCELLED,那我们就尝试把它设置为 CANCELLED。原子操作函数确保了只会有一个线程成功地把当前的 Subscription 置为 CANCELLED,而这个线程负责将其取消。注意,`cancel()` 有可能在 `onSubscribe` 之前被调用,所以它是有可能为 `null` 的,我们要检查。
 76 | 
 77 | 接下来,我们看看 `onSubscribe()`:
 78 | 
 79 | ~~~ java
 80 | @Override
 81 | public void onSubscribe(Subscription s) {
 82 |     if (!compareAndSet(null, s)) {                         // (1)
 83 |         s.cancel();                                        // (2)
 84 |         if (get() != CANCELLED) {                          // (3)
 85 |             SubscriptionHelper.reportSubscriptionSet();
 86 |         }
 87 |         return;
 88 |     }
 89 |              
 90 |     long r = MISSED_REQUESTED.getAndSet(this, 0L);         // (4)
 91 |     if (r != 0L) {                                         // (5)
 92 |         s.request(r);
 93 |     }
 94 | }
 95 | ~~~
 96 | 
 97 | 1. 我们利用 CAS 操作用新来的 Subscription 替换 null。
 98 | 2. 如果 CAS 失败,则说明已经有了 Subscription,那我们就把新来的取消掉。
 99 | 3. 有可能有些恶意的数据源会调用多次 `onSubscribe`,尽管可能性不大,但我们还是需要处理。如果此时的 Subscription 不是 CANCELLED,我们就报告这种情况的出现,然后返回。
100 | 4. 如果 CAS 成功,我们就用 `getAndSet` 读取所有积累的请求量。
101 | 5. 如果确实有积累的请求,那我们就转发给新来的 Subscription。
102 | 
103 | 最后我们看看 `request()`:
104 | 
105 | ~~~ java
106 | @Override
107 | public void request(long n) {
108 |     Subscription s = get();
109 |     if (s != null) {                                       // (1)
110 |         s.request(n);
111 |     } else {
112 |         BackpressureHelper.add(MISSED_REQUESTED, this, n); // (2)
113 |         s = get();
114 |         if (s != null && s != CANCELLED) {                 // (3)
115 |             long r = MISSED_REQUESTED.getAndSet(this, 0L); // (4)
116 |             if (r != 0L) {                                 // (5)
117 |                 s.request(r);
118 |             }
119 |         }
120 |     }
121 | }
122 | ~~~
123 | 
124 | 1. 首先我们检查当前是否已经有了 Subscription,如果有,就直接向它请求。当前的 Subscription 有可能是 CANCELLED,但没关系,它不会做任何事情。
125 | 2. 我们利用 `BackpressureHelper` 来安全累加请求计数 `missedRequested`(它会保证不发生上溢,最多加到 `Long.MAX_VALUE`)。**_2.x 的 bug:没有对 n 进行合法性检查_**。
126 | 3. 当我们累加了请求计数之后,我们还要再次检查是否已经有了 Subscription,因为它可能被异步地设置。
127 | 4. 如果当前有了 Subscription 且不是 CANCELLED,那我们就用 `getAndSet` 读出积累的请求量。这个原子调用保证了请求计数只会被 `request()` 或者 `onSubscribe()` 之一读取到。
128 | 5. 如果请求计数不为零,那我们就向 Subscription 发出请求。否则可能 `onSubscribe()` 已经请求过了,或者压根就没有请求。
129 | 
130 | `AmbInnerSubscriber.onXXX` 和 1.x 的实现基本一样。它有一个自己的变量 `boolean won`(不需要 volatile),如果是 true,就作为事件派发的快路径。否则它就会尝试把自己设置为胜利者,如果成功,就会置 won 为 true。否则就会取消订阅当前持有的 Subscription(注意这时 Subscription 不可能为 null,因为 RS 要求在 onXXX 之前必须调用 onSubscribe)。
131 | 
132 | **_2.x 的 bug:如果 AmbSubscriber 胜利了,它并不会取消其他的 AmbSubscriber,这样它们会一直保持订阅状态_**。
133 | 
134 | ## 总结
135 | 
136 | `amb` 操作符并不复杂,顶多只能算是中等水平。但它还是需要一些特殊的逻辑来处理取消订阅以及请求派发。
137 | 


--------------------------------------------------------------------------------
/_posts/2016-06-18-operator-concurrency-primitives-6.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: Operator 并发原语: producers(四),RangeProducer 优化
  4 | tags:
  5 |     - Operator
  6 |     - Producer
  7 | ---
  8 | 
  9 | 原文 [Operator concurrency primitives: producers (part 4)](http://akarnokd.blogspot.com/2015/05/operator-concurrency-primitives_13.html){:target="_blank"}
 10 | 
 11 | ## 介绍
 12 | 
 13 | 在实现了[相对复杂的 producer](/AdvancedRxJava/2016/06/11/operator-concurrency-primitives-5/index.html){:target="_blank"} 之后,现在是时候关注简单一点的内容了。在本文中,我将对最初介绍的 `RangeProducer` 进行一次优化:在无限请求时增加一个发射快路径。
 14 | 
 15 | 在 RxJava 中,如果第一次就请求 `Long.MAX_VALUE` 等同于请求无限的数据,并且会触发很多发射快路径,就像支持 backpressure 之前的远古时代那样。在这种情况下,我们无需响应请求并生产数据了(只需处理好取消订阅即可)。
 16 | 
 17 | ## `RangeProducer` 的快路径
 18 | 
 19 | 我只列出 `request()` 方法的代码,因为其他部分的代码完全没有变化:
 20 | 
 21 | ~~~ java
 22 | // ... same as before
 23 | @Override
 24 | public void request(long n) {
 25 |     if (n < 0) {
 26 |         throw new IllegalArgumentException();
 27 |     }
 28 |     if (n == 0) {
 29 |         return;
 30 |     }
 31 |     if (BackpressureUtils.getAndAddRequest(this, n) != 0) {
 32 |         return;
 33 |     }
 34 |     if (n == Long.MAX_VALUE) {                                // (1)
 35 |         if (child.isUnsubscribed()) {
 36 |             return;
 37 |         }
 38 |         int i = index;                                        // (2)
 39 |         int k = remaining;
 40 |         while (k != 0) {
 41 |             child.onNext(i);
 42 |             if (child.isUnsubscribed()) {                     // (3)
 43 |                 return;
 44 |             }
 45 |             i++;                                              // (4)
 46 |             k--;
 47 |         }
 48 |         if (!child.isUnsubscribed()) {
 49 |             child.onCompleted();
 50 |         }
 51 |         return;                                               // (5)
 52 |     }
 53 |  
 54 |     long r = n;
 55 |     for (;;) {
 56 | // ... same as before
 57 | ~~~
 58 | 
 59 | 快路径的工作原理如下:
 60 | 
 61 | 1. 如果我们成功把计数从 0 增加到 n,并且 n 为 `Long.MAX_VALUE`,我们就进入了快路径,如果 n 小于 `Long.MAX_VALUE`,我们将执行慢路径。
 62 | 2. 我们把 producer 的状态读取到局部变量中。注意,如果之前在慢路径中发射过数据,那我们读取到的值将反映出我们继续发射的位置。如果当前这次无限的请求得到了发射的权利(_当然得到了,因为现在我们已经进入了快路径_)。
 63 | 3. 检查 child 是否已经取消了订阅。
 64 | 4. 我们递增 `i`,递减 `k`。
 65 | 5. 在所有的数据以及结束事件发射完毕之后,我们就直接退出执行,而不再调整内部的请求计数。这确保了结束之后的请求既不会进入快路径,也不会进入慢路径,因为 `BackpressureUtils.getAndAddRequest` 永远不会成功。
 66 | 
 67 | 注意,小量请求后接着一个无限请求这种情况在 RxJava 中不会发生。操作符要么开启了 backpressure,要么没有开启 backpressure,所以我们无需担心,如果无限请求在慢路径循环中和 `r = addAndGet(-e);` 之间到来并且可能把请求计数递减到 `Long.MAX_VALUE` 之下,而导致我们被陷在慢路径中。
 68 | 
 69 | ## 实现一个基于数组的 producer
 70 | 
 71 | RxJava 的 `from()` 操作符支持传入一个 `T` 类型的数组,但在其内部实现中,这个数组会在 producer 中被转化为一个列表并进行遍历。这种方式看起来不必要,既然我们拿到的是一个已知长度的数组,那我们就无需 `Iterator` 而是直接利用下标进行遍历了(你可能会认为 JIT 会对此进行优化,使得 `Iterator` 在栈上进行分配,但 `onNext()` 中的代码有可能会阻止此项优化)。另外,由于 `from()` 不支持基本类型的数组,所以你可能需要自行编写一个支持此类型的操作符。
 72 | 
 73 | `RangeProducer` 的结构是实现这个功能的一个不错的选择:我们可以用 `index` 来记录当前遍历到数组的下标,然后把它和数组长度进行对比以决定何时退出。
 74 | 
 75 | ~~~ java
 76 | public final class ArrayProducer 
 77 | extends AtomicLong implements Producer {
 78 |     /** */
 79 |     private static final long serialVersionUID = 1L;
 80 |     final Subscriber child;
 81 |     final int[] array;                                        // (1)
 82 |     int index;
 83 |     public ArrayProducer(Subscriber child, 
 84 |             int[] array) {
 85 |         this.child = child;
 86 |         this.array = array;
 87 |     }
 88 |     @Override
 89 |     public void request(long n) {
 90 |         if (n < 0) {
 91 |             throw new IllegalArgumentException();
 92 |         }
 93 |         if (n == 0) {
 94 |             return;
 95 |         }
 96 |         if (BackpressureUtils.getAndAddRequest(this, n) != 0) {
 97 |             return;
 98 |         }
 99 |         final int[] a = this.array;
100 |         final int k = a.length;                               // (2)
101 |         if (n == Long.MAX_VALUE) {
102 |             if (child.isUnsubscribed()) {
103 |                 return;
104 |             }
105 |             int i = index;
106 |             while (i != k) {                                  // (3)
107 |                 child.onNext(a[i]);
108 |                 if (child.isUnsubscribed()) {
109 |                     return;
110 |                 }
111 |                 i++;
112 |             }
113 |             if (!child.isUnsubscribed()) {
114 |                 child.onCompleted();
115 |             }
116 |             return;
117 |         }
118 |         long r = n;
119 |         for (;;) {
120 |             if (child.isUnsubscribed()) {
121 |                 return;
122 |             }
123 |             int i = index;
124 |             int e = 0;
125 |              
126 |             while (r > 0 && i != k) {
127 |                 child.onNext(a[i]);
128 |                 if (child.isUnsubscribed()) {
129 |                     return;
130 |                 }
131 |                 i++;
132 |                 if (i == k) {                               // (4)
133 |                     child.onCompleted();
134 |                     return;
135 |                 }
136 |                 e++;
137 |                 r--;
138 |             }
139 |             index = i;
140 |              
141 |             r = addAndGet(-e);
142 |              
143 |             if (r == 0) {
144 |                 return;
145 |             }
146 |         }
147 |     }
148 | }
149 |  
150 | int[] array = new int[200];
151 | Observable source = Observable.create(child -> {
152 |     if (array.length == 0) {
153 |         child.onCompleted();
154 |         return;
155 |     }
156 |     ArrayProducer ap = new ArrayProducer(child, array);
157 |     child.setProducer(ap);
158 | });
159 | source.subscribe(System.out::println);
160 | ~~~
161 | 
162 | 1. 除了 `index` 之外,我们还需要 `array` 来保存待发射的数组,我们无需 `remaining` 了,因为 `index` 最多递增到数组的长度。
163 | 2. 结束运行的条件是局部变量 `i` 递增到 `k`(数组长度)。注意我们无需递减 `k`。
164 | 3. 在快路径中,在 `i` 递增到数组长度之前我们都进行循环。
165 | 4. 在慢路径中,每次递增 `i` 之后,我们立即检查是否已经抵达了数组的末尾,如果抵达末尾就发出 `onCompleted()`。注意,慢路径中不支持空数组。
166 | 
167 | ## 总结
168 | 
169 | 在本文中,我展示了如何为简单如 `RangeProducer` 的 producer 增加一个快路径,并且如何把它转变为一个支持基本类型数组的 producer,避免额外的 `Iterator` 分配和遍历开销。
170 | 
171 | 到目前为止,我介绍了众多的 producer,包括确切知道应该发射多少数据的 producer,以及不知道或者不关心发射量的 producer。然而,存在一些需要处理来自多种 producer 的多个数据源的操作符,它们还需要处理得 child 只需要处理一种数据源。在下一篇关于 producer 的文章中,我将介绍一种我称之为 **producer-arbiter** 的 producer,它能在保证 backpressure 的前提下支持不同 producer 之间进行切换。
172 | 


--------------------------------------------------------------------------------
/_posts/2016-07-29-operator-concurrency-primitives-subscription-containers-3.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: Operator 并发原语: subscription-containers(三,完结),基于数组的容器类
  4 | tags:
  5 |     - Operator
  6 |     - Subscription
  7 | ---
  8 | 
  9 | 原文 [Operator concurrency primitives: subscription-containers (part 3 - final)](http://akarnokd.blogspot.com/2015/05/operator-concurrency-primitives_26.html){:target="_blank"}
 10 | 
 11 | ## 介绍
 12 | 
 13 | 在最后一篇关于 subscription 容器类的文章中(_本文_),我将介绍一种基于数组的 copy-on-write 线程安全容器类。
 14 | 
 15 | 为什么这一容器类型如此重要?让我反问一句:如果被包含的 subscription 是 `Subscriber`,我们如何处理 `Subscriber` 数组?
 16 | 
 17 | 你可以实现一个支持给多个子 Subscriber 组播的操作符,同时保证线程安全性和终结状态,就像 `Subject` 处理其 Subscriber 一样,以及像最近(_2015年5月_)对 `publish()` 的重新实现那样。
 18 | 
 19 | ## 基于数组的容器类
 20 | 
 21 | 让我们实现这样一个容器类:
 22 | 
 23 | + 能添加和移除某种 subscription,例如 `Subscriber`;
 24 | + 能获取当前的内容(_包含的 subscription_);
 25 | + 能够在不取消订阅其包含的 subscription 的前提下,取消订阅容器类;
 26 | + 添加 subscription 能得知是否成功;
 27 | 
 28 | 它的类结构如下:
 29 | 
 30 | ~~~ java
 31 | @SuppressWarnings({"rawtypes", "unchecked"})                 // (1)
 32 | public class SubscriberContainer {
 33 |     static final Subscriber[] EMPTY = new Subscriber[0];     // (2)
 34 |     static final Subscriber[] TERMINATE = new Subscriber[0];
 35 |      
 36 |     final AtomicReference array
 37 |         = new AtomicReference<>(EMPTY);                      // (3)
 38 |      
 39 |     public Subscriber[] get() {                           // (4)
 40 |         return array.get();
 41 |     }
 42 |  
 43 |     public boolean add(Subscriber s) {                    // (5)
 44 |         // implement
 45 |     }
 46 |  
 47 |     public boolean remove(Subscriber s) {                 // (6)
 48 |         // implement
 49 |     }
 50 |  
 51 |     public Subscriber[] getAndTerminate() {               // (7)
 52 |         return array.getAndSet(TERMINATE);
 53 |     }
 54 |  
 55 |     public boolean isTerminated() {                          // (8)
 56 |         return get() == TERMINATED;
 57 |     }
 58 | }
 59 | ~~~
 60 | 
 61 | 它包含以下几个元素:
 62 | 
 63 | 1. 由于 Java 的数组不支持泛型,并且特定的类型转换不符合规范,所以我们必须掩盖 rawtypes 以及 unchecked 类型转换(为泛型类型)的警告。通常来说,我们的实现是安全的(_不存在类型转换失败_),但我们需要让用户的代码具备类型安全性。
 64 | 2. 我们用数组常量来表示初始状态和终结状态。
 65 | 3. 我们用 `AtomicReference` 来引用数组,如果我们知道这个容器类不会被继承,那我们可以直接继承自 `AtomicReference`。
 66 | 4. `get()` 函数返回当前包含的元素。处于性能考虑,该函数的返回值只能用于读访问(否则就需要在每次返回之前进行深拷贝)。
 67 | 5. `add()` 函数接收带有类型的 `Subscriber`,如果添加成功就返回 `true`,否则如果容器类已经被取消订阅,就返回 `false`。
 68 | 6. `remove()` 尝试移除指定的 `Subscriber`,如果移除成功就返回 `true`。
 69 | 7. 这里没有 `unsubscribe()` 函数,而是取了个巧:我们把容器类的数组替换为终结状态,并把原来的值返回。当我们需要原子性终结,但在终结之后需要进行其他操作时,这一方式就很有用了。
 70 | 8. 由于一个空的数组不能表明处于终结状态(无法和初始状态进行区分),所以我们需要和 `TERMINATED` 对比引用。
 71 | 
 72 | `add()` 很简单:
 73 | 
 74 | ~~~ java
 75 | // ...
 76 | public boolean add(Subscriber s) {
 77 |     for (;;) {
 78 |         Subscriber[] current = array.get();
 79 |         if (current == TERMINATED) {                  // (1)
 80 |             return false;
 81 |         }
 82 |         int n = current.length;
 83 |         Subscriber[] next = new Subscriber[n + 1];
 84 |         System.arraycopy(current, 0, next, 0, n);     // (2)
 85 |         next[n] = s;
 86 |         if (array.compareAndSet(current, next)) {     // (3)
 87 |             return true;
 88 |         }
 89 |     }
 90 | }
 91 | // ...
 92 | ~~~
 93 | 
 94 | 这里又是一个典型的 CAS 循环:
 95 | 
 96 | 1. 如果容器类已经处于终结状态,那我们就直接返回 `false`,待添加的 Subscriber 也不会被添加。调用方可以根据返回值决定如何处理待添加的 Subscriber,例如向其发送 `onCompleted` 事件。
 97 | 2. 我们为数组扩容,拷贝已经存在的 Subscriber,并把新的添加在最后。
 98 | 3. CAS 操作相当于把修改进行一次提交操作,如果成功,我们就返回 `true`,否则我们就继续尝试。
 99 | 
100 | 最后是 `remove()` 函数:
101 | 
102 | ~~~ java
103 | // ...
104 | public boolean remove(Subscriber s) {
105 |     for (;;) {
106 |         Subscriber[] current = array.get();
107 |         if (current == EMPTY 
108 |                 || current == TERMINATED) {             // (1)
109 |             return false;
110 |         }
111 |         int n = current.length;
112 |         int j = -1;
113 |         for (int i = 0; i < n; i++) {                   // (2)
114 |             Subscriber e = current[i];
115 |             if (e.equals(s)) {
116 |                 j = i;
117 |                 break;
118 |             }
119 |             i++;
120 |         }
121 |         if (j < 0) {                                    // (3)
122 |             return false;
123 |         }
124 |         Subscriber[] next;
125 |         if (n == 1) {                                   // (4)
126 |             next = EMPTY;
127 |         } else {
128 |             next = new Subscriber[n - 1];
129 |             System.arraycopy(current, 0, next, 0, j);
130 |             System.arraycopy(current, j + 1, 
131 |                 next, j, n - j - 1);                    // (5)
132 |         }
133 |         if (array.compareAndSet(current, next)) {       // (6)
134 |             return true;
135 |         }
136 |     }
137 | }
138 | ~~~
139 | 
140 | 尽管代码看起来有点复杂,但是它的逻辑还是很直观的:
141 | 
142 | 1. 如果当前数组是空的,或者容器类已经终止了,那我们就可以直接返回 `false` 了。
143 | 2. 否则我们就从前往后搜索到第一个要移除的 Subscriber(用 `equals` 判断),并把它的下标记为 `j`。(_这里省略了一句原文,“By scanning first instead of doing an on-the-fly filter-copy, we can save some overhead due to the card-marking associated with each reference store, required by most GCs”,和作者沟通之后,大意就是:“我们先查找,再分段复制,而不是先创建一个数组,再逐个判断复制,这样可以快一些,性能差异还涉及到一些关于引用存储相关的原理”,先查找再分段复制,大家应该都会这么干,不然你新建一个多大的数组呢?都不知道能不能找到呢!但是引用存储的原理什么的,这里我就不懂,暂时也不想懂了,有兴趣可以自行扩展_)。
144 | 3. 查找结束之后,如果 `j` 是负数,说明指定的 Subscriber 不在数组中,所以我们返回 `false`。
145 | 4. 如果原数组就只有一个元素,那我们就无需创建一个新的空数组,我们可以直接复用 `EMPTY` 常量(因为空数组没有状态,意义都是一样的)。
146 | 5. 如果原数组不止一个元素,那我们就创建一个长度减一的新数组,并把目标周围的元素都复制过去。
147 | 6. 最终 CAS 操作会尝试替换新的数组,如果成功就返回 `true`,这意味着我们成功移除了指定的 Subscriber。
148 | 
149 | 像上面这种容器,在类似于组播的操作符中用得比较多,但是这些操作符的使用场景中,基本不会涉及到超过 6 个 Subscriber,所以频繁的创建数组对象影响并不大。
150 | 
151 | 如果在你的使用场景下数组创建比例很高进而产生了性能问题,那你可以把上述逻辑改成基于 `synchronized` 块的实现,并使用 `List` 或者 `Set` 这样的数据结构存储 Subscriber,但要注意在派发事件时会被频繁调用的 `get()` 方法,这时就不能用非阻塞的方式实现了。这时 `get()` 很可能需要利用 `synchronized` 块实现,并且还需要进行 defensive copy,所以你需要**仔细权衡考虑**。
152 | 
153 | ## 总结
154 | 
155 | 在这个迷你系列中,我介绍了几种标准的 subscription 容器类型,并且展示了如何实现阻塞和非阻塞的自定义容器。最后我展示了一种基于数组的非阻塞容器,它的一些看似奇特的特性在组播类操作符中非常有用。
156 | 
157 | 如果我们只有 RxJava 1.x,那操作符并发原语(operator concurrency primitives)系列就可以结束了。但 reactive-streams 标准最近已经定稿,而它将是 RxJava 2.x 的基石,所以这些并发原语都需要完全重写,无法避免。
158 | 
159 | 这是否意味着前面这么多的东西都是白学的?是也不是。实际上涉及到的一些理念都是相通的,只是类的结构需要根据 reactive-streams 规范进行调整。
160 | 
161 | 在开始 RxJava 2.x 相关的内容之前,让我们稍事休息,先看看其他进阶话题的内容:**调度器(schedulers)**。
162 | 


--------------------------------------------------------------------------------
/_posts/2016-06-25-pitfalls-of-operator-implementations-2.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: 实现操作符时的一些陷阱(二)
  4 | tags:
  5 |     - Operator
  6 |     - Pitfall
  7 | ---
  8 | 
  9 | 原文 [Pitfalls of operator implementations (part 2)](http://akarnokd.blogspot.com/2015/05/pitfalls-of-operator-implementations_14.html){:target="_blank"}
 10 | 
 11 | ## 介绍
 12 | 
 13 | 本文中我将暂停对 producer 的讲解,继续回到实现操作符的陷阱这个话题,而且还会提到使用特定(序列)的 RxJava 操作符时的一些陷阱。
 14 | 
 15 | ## 6,不等请求就直接发射(Emitting without request)
 16 | 
 17 | 假设你要实现一个操作符,它会忽略上游发出的任何数据,并在上游结束时发出一个固定的值:
 18 | 
 19 | ~~~ java
 20 | Operator ignoreAllAndJust = child -> {
 21 |     Subscriber parent = new Subscriber() {
 22 |         @Override
 23 |         public void onNext(Integer value) {
 24 |             // ignored
 25 |         }
 26 |         @Override
 27 |         public void onError(Throwable e) {
 28 |             child.onError(e);
 29 |         }
 30 |         @Override
 31 |         public void onCompleted() {
 32 |             child.onNext(1);
 33 |             child.onCompleted();
 34 |         }
 35 |     };
 36 |     child.add(parent);
 37 |     return parent;
 38 | };
 39 | ~~~
 40 | 
 41 | 上面的这个操作符依赖于两个前提:`Subscriber` 的默认行为就是发出请求;下游一定会在上游结束之前至少发出一次请求。然而,即便你的测例通过了,这个操作符依然违反了 backpressure 的要求:`onCompleted()` 函数无条件的发出了一个数据,没有检查下游是否发出过请求。这个问题会在这样的场景下暴露出来:如果你有一个 hot `Observable` 或者不考虑 backpressure 的 `Observable`,而你又需要和 [reactive-streams](https://github.com/reactive-streams/reactive-streams-jvm){:target="_blank"} 兼容的下游进行交互,那么下游的 `Subscriber` 就会收到 `onError` 了,因为你的行为违反了 reactive-streams 规则的 §1.1 节。
 42 | 
 43 | 既然我们现在已经了解了很多 producer,修复这个问题非常简单:
 44 | 
 45 | ~~~ java
 46 | // ... same as before
 47 | @Override
 48 | public void onCompleted() {
 49 |     child.setProducer(new SingleProducer(child, 1));
 50 | }
 51 | // ... same as before
 52 | ~~~
 53 | 
 54 | 我们在 [produer(二)](/AdvancedRxJava/2016/06/04/operator-concurrency-primitives-4/index.html){:target="_blank"} 中介绍过 `SingleProducer`,现在它是最合适的选择。
 55 | 
 56 | 但是我想介绍另外一种解决方案,这种方案和 RxJava 2.0 以及 reactive-streams 兼容的操作符相关:
 57 | 
 58 | ~~~ java
 59 | Operator ignoreAllAndJust = child -> {
 60 |     SingleDelayedProducer sdp = 
 61 |         new SingleDelayedProducer<>(child);
 62 |     Subscriber parent = new Subscriber() {
 63 |         @Override
 64 |         public void onNext(Integer value) {
 65 |             // ignored
 66 |         }
 67 |         @Override
 68 |         public void onError(Throwable e) {
 69 |             child.onError(e);
 70 |         }
 71 |         @Override
 72 |         public void onCompleted() {
 73 |             sdp.set(1);
 74 |         }
 75 |     };
 76 |     child.add(parent);
 77 |     child.setProducer(sdp);
 78 |     return parent;
 79 | };
 80 | ~~~
 81 | 
 82 | 这种方案功能上是一样的,尽管相较于 RxJava 1.x 稍显冗长。之所以需要这样,是因为操作符的 Subscriber 无法脱离 Producer 而单独存在。而这是因为 Producer 语义上来说也是一种 Subscription,而且它为 Subscriber 提供了从上游取消订阅的唯一途径。延迟设置 producer 会延迟可能的取消订阅。
 83 | 
 84 | ## 7,操作符中的共享状态(Shared state in the operator)
 85 | 
 86 | 你可能认为 `ignoreAllAndJust` 很傻也没什么用处,但如果我们把它改成一个在接收到上游数据时进行计数,并在上游结束时发出这个计数,那它就变得有点用处了。假设我们的编译环境是 Java 6,不能用 lambda 表达式:
 87 | 
 88 | ~~~ java
 89 | public final class CounterOp {
 91 |     int count;                                              // (1)
 92 |     @Override
 93 |     public Subscriber call(Subscriber child) {
 94 |         Subscriber parent = new Subscriber() {
 95 |             @Override
 96 |             public void onNext(T t) {
 97 |                 count++;
 98 |             }
 99 |             @Override
100 |             public void onError(Throwable e) {
101 |                 child.onError(e);
102 |             }
103 |             @Override
104 |             public void onCompleted() {
105 |                 child.setProducer(
106 |                     new SingleProducer(child, count));
107 |             }
108 |         };
109 |         child.add(parent);
110 |         return parent;
111 |     }
112 | }
113 |  
114 | Observable count = Observable.just(1)
115 |     .lift(new CounterOp());                        // (2)
116 |          
117 | count.subscribe(System.out::println);
118 | count.subscribe(System.out::println);
119 | count.subscribe(System.out::println);
120 | ~~~
121 | 
122 | 我们已经吸取了上一条的教训,正确实现了 `onCompleted()` 方法,然而如果运行上面的代码,我们会发现打印的结果是 `1`,`2` 和 `3`!显然 `just(1)` 的计数应该始终是 `1`,无论我们对它计数多少次。
123 | 
124 | 问题就出在(1)处,我们在所有的订阅者中共享了 `count` 变量。第一个订阅者会把它增加到 `1`,第二个订阅者会把它增加到 `2`,以此类推,由于(2),我们始终只有一个 `CounterOp` 实例,因此也就只有一个 `count` 实例。
125 | 
126 | 解决办法就是把 `count` 移到 `parent` 中:
127 | 
128 | ~~~ java
129 | public final class CounterOp {
131 |     @Override
132 |     public Subscriber call(Subscriber child) {
133 |         Subscriber parent = new Subscriber() {
134 |            int count;
135 |     // ... the rest is the same
136 | ~~~
137 | 
138 | 当然我们也有一些场景需要在订阅者之间共享变量,但这些场景少之又少,所以第一原则就是:`Operator` 的所有成员都声明为 `final`。一旦声明为 `final`,你很快就会发现你的代码在尝试修改它们(_你也很快就会发现代码写得有 bug_)。
139 | 
140 | ## 8,Observable 链条中的共享状态(Shared state in an Observable chain)
141 | 
142 | 假设你对 `toList()` 的性能不满意,或者它返回的 `List` 类型不满足需求,你打算实现一个自己的聚合器。你希望通过已有的操作符解决这个问题,你找到了 `reduce()`:
143 | 
144 | ~~~ java
145 | Observable> list = Observable
146 |     .range(1, 3)
147 |     .reduce(new Vector(), (vector, value) -> {
148 |         vector.add(value);
149 |         return vector;
150 |     });
151 |  
152 | list.subscribe(System.out::println);
153 | list.subscribe(System.out::println);
154 | list.subscribe(System.out::println);
155 | ~~~
156 | 
157 | 如果运行上面的代码,你会发现第一次打印符合预期,但第二次打印了两遍,第三次则打印了三遍!
158 | 
159 | 问题不是出在 `reduce()` 本身,而是对它的使用方式。当链条建立起来之后,传入 `reduce()` 的 `Vector` 实例就相当于一个“全局”的了,后续对这个链条的调用都会共用同一个实例。
160 | 
161 | 修复我们遇到的这个具体问题很简单,无需重新实现一个操作符:
162 | 
163 | ~~~ java
164 | Observable> list2 = Observable
165 |     .range(1, 3)
166 |     .reduce((Vector)null, (vector, value) -> {
167 |         if (vector == null) {
168 |             vector = new Vector<>();
169 |         }
170 |         vector.add(value);
171 |         return vector;
172 |     });
173 |  
174 | list2.subscribe(System.out::println);
175 | list2.subscribe(System.out::println);
176 | list2.subscribe(System.out::println);
177 | ~~~
178 | 
179 | 你需要传入 `null`,并在聚合函数内创建新的 `Vector` 实例,这样就不会在订阅者之间共享了。
180 | 
181 | 首要原则是,对于任何需要传入一个初始值的聚合操作符,都需要小心,很可能这个初始值是被不同订阅者所共享的,而如果你想要把链式调用的结果用多个订阅者去消费,它们就会发生冲突了,可能会导致不可预测的行为,甚至崩溃。
182 | 
183 | ## 总结
184 | 
185 | 在本文中,我分析了三种关于操作符更常见的陷阱,并且展示了如何测试并修复背后的 bug。
186 | 
187 | 在使用 RxJava 各种操作符的过程中,可能很容易遇到各种奇怪(甚至滑稽)的问题(至少对我是这样),所以我的追求远未停止。而新的问题也变得越来越微妙,为了解决这些问题,我们需要了解更多关于操作符的原理。
188 | 


--------------------------------------------------------------------------------
/_includes/footer.html:
--------------------------------------------------------------------------------
  1 | 
  2 | 
 76 | 
 77 | 
 78 | 
 79 | 
 80 | 
 81 | 
 82 | 
 83 | 
 84 | 
 85 | 
 86 | 
 87 | 
 88 | 
 98 | 
 99 | 
100 | 
115 | 
116 | 
117 | 
118 | 
124 | 
125 | 
126 | 
127 | {% if site.ga_track_id %}
128 | 
142 | {% endif %}
143 | 
144 | 
145 | 
146 | {% if site.ba_track_id %}
147 | 
160 | {% endif %}
161 | 


--------------------------------------------------------------------------------
/_posts/2016-08-19-schedulers-2.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: 调度器 Scheduler(二):自定义 ExecutorService 使用逻辑的 Worker
  4 | tags:
  5 |     - Scheduler
  6 | ---
  7 | 
  8 | 原文 [Schedulers (part 2)](http://akarnokd.blogspot.com/2015/06/schedulers-part-2.html){:target="_blank"}
  9 | 
 10 | ## 介绍
 11 | 
 12 | 在[上文中](/AdvancedRxJava/2016/08/05/schedulers-1/index.html){:target="_blank"},我介绍了如何利用 RxJava 已有的类来实现自定义的 scheduler。
 13 | 
 14 | 在本文中,我将更加深入一层,演示如何操控底层的 `ExecutorService` 以及 RxJava 其他的基础设施,并与之进行交互,而这些都是无法通过 `NewThreadWorker` 实现的。
 15 | 
 16 | ## `ScheduledAction` 类
 17 | 
 18 | 在 `Scheduler`/`Worker` 诞生之前的日子里,和 `Future` 类进行交互是非常直观的:我们只需要用一个 `Subscription` 类包装 `cancel()` 就可以了,这样它就能被加入到各种 subscription 容器类中了。
 19 | 
 20 | 然而引入了 `Scheduler`/`Worker` 之后就不能这样了,因为我们需要记录这些任务,并且取消它们。一旦 `Future` 被记录了,那就需要在它们完成或者被取消时取消记录,否则就会发生内存泄漏。这一要求就意味着我们不能直接把一个 `Action0`/`Runnable` 提交到 `ExecutorService` 上,我们需要包装一下它们,让它们可以在完成或者被取消时被取消记录。
 21 | 
 22 | 解决方案就是 `ScheduledAction` 类。所有常规的 `Action0` 都会在 `NewThreadWorker.scheduleActual()` 中被包装为 `ScheduledAction`。它的内部包含了一个 `SubscriptionList`,用来容纳那些在这个任务完成或者取消时要执行的操作:
 23 | 
 24 | ~~~ java
 25 | public final class ScheduledAction
 26 | implements Runnable, Subscription {
 27 |     final Action0 action;                       // (1)
 28 |     final SubscriptionList slist;               // (2)
 29 |  
 30 |     public ScheduledAction(Action0 action) {
 31 |         this.action = action;
 32 |         this.slist = new SubscriptionList();
 33 |     }
 34 |     @Override
 35 |     public void run() {
 36 |         try {
 37 |             action.call();                      // (3)
 38 |         } finally {
 39 |             unsubscribe();                      // (4)
 40 |         }
 41 |     }
 42 |     @Override
 43 |     public boolean isUnsubscribed() {
 44 |         return slist.isUnsubscribed();
 45 |     }
 46 |     @Override
 47 |     public void unsubscribe() {
 48 |         slist.unsubscribe();
 49 |     }
 50 |      
 51 |     public void add(Subscription s) {           // (5)
 52 |         slist.add(s);
 53 |     }
 54 | }
 55 | ~~~
 56 | 
 57 | 这个类还是非常直观的:
 58 | 
 59 | 1. 我们保存真正要被执行的任务。
 60 | 2. 我们需要一个容器类来保存所有取消订阅时要执行的任务,由于它只会被增加,所以 `SubscriptionList` 就足够了。
 61 | 3. 由于 `ExecutorService` 接收的是 `Runnable`,所以我们实现 `Runnable` 接口,并在 `run()` 函数中执行实际的任务。
 62 | 4. 无论任务执行成功与否,我们都调用 `unsubscribe()` 函数,用来触发执行清理任务。
 63 | 5. 但是这些清理任务需要注册到 `ScheduledAction` 中,所以我们暴露出 `SubscriptionList` 的 `add()` 函数。
 64 | 
 65 | 接下来就是要在任务被提交到 `ExecutorService` 之前把所有的记录以及清理任务都串起来。为了简单起见,我们假设我们的 ExecutorService 是个单线程的 service。我们将在后面处理多线程的情况。首先让我们看一下自定义 `Worker` 的结构:
 66 | 
 67 | ~~~ java
 68 | public final class CustomWorker 
 69 | extends Scheduler.Worker {
 70 |     final ExecutorService exec;                             // (1)
 71 |     final CompositeSubscription tracking;                   // (2)
 72 |     final boolean shutdown;                                 // (3)
 73 |      
 74 |     public CustomWorker() {
 75 |         exec = Executors.newSingleThreadExecutor();
 76 |         tracking = new CompositeSubscription();
 77 |         shutdown = true;
 78 |     }
 79 |     public CustomWorker(ExecutorService exec) {
 80 |         this.exec = exec;
 81 |         tracking = new CompositeSubscription();
 82 |         shutdown = false;                                   // (4)
 83 |     }
 84 |     @Override
 85 |     public Subscription schedule(Action0 action) {
 86 |         return schedule(action, 0, null);                   // (5)
 87 |     }
 88 |     @Override
 89 |     public Subscription schedule(Action0 action,
 90 |             long delayTime, TimeUnit unit) {
 91 |         // implement
 92 |     }
 93 |     @Override
 94 |     public boolean isUnsubscribed() {
 95 |         return tracking.isUnsubscribed();                   // (6)
 96 |     }
 97 |     @Override
 98 |     public void unsubscribe() {
 99 |         if (shutdown) {
100 |             exec.shutdownNow();                             // (7)
101 |         }
102 |         tracking.unsubscribe();
103 |     }
104 | }
105 | ~~~
106 | 
107 | 目前为止,这个结构还不复杂:
108 | 
109 | 1. 我们保存实际的线程池的引用。
110 | 2. 我们还需要保存提交过来的任务的引用,以便于后面取消它们。
111 | 3. 我们也打算支持用户使用自己的单线程 service,在这种情况下,关闭 service 就是调用者的责任了。
112 | 4. 我们只关闭我们自己创建的 service,不关闭在构造函数中传入的 service。
113 | 5. 我们把无延迟的调度转发为一个延迟为 0 的调度。
114 | 6. `tracking` 成员还记录被提交的任务是否已经被取消。
115 | 7. 如果 `ExecutorService` 是我们自己创建的,那我们就将它关闭,然后取消订阅所有提交的任务(注意,如果 service 是我们自己创建的,那其实我们不需要记录提交过来的任务,因为 service 已经记录了,我们可以在 `shutdownNow()` 中取消订阅它们)。
116 | 
117 | 最后,我们看一下延迟 `schedule()` 的实现:
118 | 
119 | ~~~ java
120 | // ...
121 | @Override
122 | public Subscription schedule(Action0 action, 
123 |         long delayTime, TimeUnit unit) {
124 |     if (isUnsubscribed()) {                                // (1)
125 |         return Subscriptions.unsubscribed();
126 |     }
127 |     ScheduledAction sa = new ScheduledAction(action);      // (2)
128 |      
129 |     tracking.add(sa);                                      // (3)
130 |     sa.add(Subscriptions.create(
131 |         () -> tracking.remove(sa)));
132 |      
133 |     Future f;
134 |     if (delayTime <= 0) {                                  // (4)
135 |         f = exec.submit(sa);
136 |     } else if (exec instanceof ScheduledExecutorService) { // (5)
137 |         f = ((ScheduledExecutorService)exec)
138 |              .schedule(sa, delayTime, unit);
139 |     } else {
140 |         f = genericScheduler.schedule(() -> {              // (6)
141 |             Future g = exec.submit(sa);
142 |             sa.add(Subscriptions.create(                   // (7)
143 |                 () -> g.cancel(false)));
144 |         }, delayTime, unit);
145 |     }
146 |      
147 |     sa.add(Subscriptions.create(                           // (8)
148 |         () -> f.cancel(false)));
149 |  
150 |     return sa;                                             // (9)
151 | }
152 | // ...
153 | ~~~
154 | 
155 | 本文第一段复杂的代码工作机制如下:
156 | 
157 | 1. 如果 worker 已经被取消订阅,我们就返回一个表示已经取消的常量 subscription。注意,如果有 schedule 调用(_由于多线程竞争_)通过了这个检查,它将会收到一个来自底层线程池的 `RejectedExecutionException`。你可以把函数中后面的代码都用一个 `try-catch` 包裹起来,并在异常发生时返回同样的表示已经取消的常量 subscription。
158 | 2. 我们把任务包装为 `ScheduledAction`。
159 | 3. 在实际调度这个任务之前,我们把它加入到 `tracking` 中,并且增加一个取消订阅的回调,以便在它执行完毕或者被取消时可以将其从 `tracking` 中移除。注意,由于幂等性,`remove()` 不会调用 `ScheduledAction` 的 `unsubscribe()`,从而不会导致死循环。
160 | 4. 如果调度是没有延迟的,我们就立即将其提交,并且保存返回的 `Future`。
161 | 5. 如果我们的 `ExecutorService` 是 `ScheduledExecutorService`,我们就可以直接调用它的 `schedule()` 函数了。
162 | 6. 否则我们就需要借助 `ScheduledExecutorService` 来实现延迟调度了,但我们不能直接把任务调度给它,因为这样它会在错误的线程中执行。我们需要创建一个中间任务,它将在延迟结束之后,向正确的线程池调度一个即时的任务。
163 | 7. 我们需要保证提交后返回的 `Future` 能在 `unsubscribe()` 调用时被取消。这里我们把内部的 `Future` 加入到了 `ScheduledAction` 中。
164 | 8. 无论是立即调度,还是延迟调度,我们都需要在取消订阅时取消这个调度,所以我们把返回的 `Future` 加入到 `ScheduledAction` 中(_通过把 `Future#cancel()` 包装到一个 `Subscription` 中_)。在这里,你就可以控制是否需要强行(_中断_)取消了。(RxJava 会根据取消订阅时所处的线程来决定:如果取消订阅就是在执行任务的线程中,那就没必要中断了)
165 | 9. `ScheduledAction` 也是任务发起方用来取消订阅的凭证(token)。
166 | 
167 | 由于 subscription 容器类的终结状态特性,即便(7)和(8)发生在 `ScheduledAction` 被取消订阅之后,它们也会立即被取消订阅。至于更加激进的需求,你可以在 `ScheduledAction#run()` 中在执行实际任务之前检查是否已经被取消订阅。
168 | 
169 | 最后缺失的一点代码就是 `genericScheduler` 了。你可以为 worker 添加一个 `static final` 成员,并像下面这样设置:
170 | 
171 | ~~~ java
172 | // ...
173 | static final ScheduledExecutorService genericScheduler;
174 | static {
175 |     genericScheduler = Executors.newScheduledThreadPool(1, r -> {
176 |         Thread t = new Thread(r, "GenericScheduler");
177 |         t.setDaemon(true);
178 |         return t;
179 |     });
180 | }
181 | // ...
182 | ~~~
183 | 
184 | ## 结论
185 | 
186 | 在本文中,我演示了如何把一个任务包装为一个可以被调度的任务,并且如何让它们可以在单个任务层面以及整个 worker 层面被取消。
187 | 
188 | 在本系列的最后一篇文章中,我将讲解如何处理多线程的 `ExecutorService`,因为我们不能让非延迟调度的任务执行时乱序甚至并发执行。
189 | 


--------------------------------------------------------------------------------
/_posts/2016-10-02-the-reactive-streams-api-part-4.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: Reactive-Streams API(四,完结):SubscriptionArbiter 的使用
  4 | tags:
  5 |     - Reactive Stream
  6 | ---
  7 | 
  8 | 原文 [The Reactive-Streams API (part 4 - final)](http://akarnokd.blogspot.com/2015/06/the-reactive-streams-api-part-4-final.html){:target="_blank"}
  9 | 
 10 | ## 介绍
 11 | 
 12 | 在这篇介绍 Reactive-Streams API 的最后一篇文章中,我会讲讲我们对 `SubscriptionArbiter`(`ProducerArbiter` 的一个兄弟) 的高频使用需求,这一点可能会让很多人感到惊讶,当涉及到多个数据源、调度、其他异步内容时,我们将会很频繁的用到它。
 13 | 
 14 | 在 RxJava 1.x 中,给 `Subscriber` 设置 `Producer` 是可选的,我们可以在没有 `Producer` 的情况下直接调用 `onError()` 和 `onCompleted()`。这些调用最终都会调用 `rx.Subscriber.unsubscribe()`,并清理相关资源。
 15 | 
 16 | 与之相反,RS 要求 `Subscription` 必须在 `onXXX` 被调用之前,通过 `onSubscribe()` 传递给 `Subscriber`。
 17 | 
 18 | 在本文中,我将展示一些这一要求可能导致的困境,尤其是当操作符的实现方式是典型的 RxJava 结构时。
 19 | 
 20 | ## 稍后订阅
 21 | 
 22 | `defer()` 是必须考虑“订阅之前的错误”(error-before-Subscription)情况的操作符之一。当订阅一个推迟的 `Publisher` 时,操作符会先利用用户提供的工厂方法创建一个新的 `Publisher`,这个新的才是之后被订阅的。由于我们必须对用户的代码进行检查,所以我们就要捕获可能的异常,并且把异常通知给下游。但是,在这种情况下(要调用 `child.onError()`),我们就需要 child Subscriber 已经有 Subscription 了,但如果 child 这时已经有了 Subscription,那新生成的 Producer 收到 Subscription 时就不能转交给 child 了。解决方案就是使用一个 `SubscriptionArbiter`,它让我们可以提前发送错误事件,或者稍后切换到“真正的”数据源。
 23 | 
 24 | (_译者注:这里 SubscriptionArbiter 相当于一个占位符或者说管道,Subscriber 不能替换 Subscription,但 SubscriptionArbiter 可以,这种移花接木的思路很赞,像国内很多插件化/热修复的方案,使用自定义的 ClassLoader,想法上差不多_)
 25 | 
 26 | ~~~ java
 27 | public final class OnSubscribeDefer
 28 | implements OnSubscribe {
 29 |     final Func0> factory;
 30 |     public OnSubscribeDefer(
 31 |            Func0> factory) {
 32 |         this.factory = factory;
 33 |     }
 34 |     @Override
 35 |     public void call(Subscriber child) {
 36 |          
 37 |         SubscriptionArbiter sa = new SubscriptionArbiter();
 38 |         child.onSubscribe(sa);                                 // (1)  
 39 |          
 40 |         Publisher p;
 41 |         try {
 42 |             p = factory.call();
 43 |         } catch (Throwable e) {
 44 |             Exceptions.throwIfFatal(e);
 45 |             child.onError(e);                                  // (2)
 46 |             return;
 47 |         }
 48 |         p.subscribe(new Subscriber() {                      // (3)
 49 |             @Override
 50 |             public void onSubscribe(Subscription s) {
 51 |                 sa.setSubscription(s);                         // (4)
 52 |             }
 53 |  
 54 |             @Override
 55 |             public void onNext(T t) {
 56 |                 child.onNext(t);
 57 |             }
 58 |  
 59 |             @Override
 60 |             public void onError(Throwable t) {
 61 |                 child.onError(t);
 62 |             }
 63 |  
 64 |             @Override
 65 |             public void onComplete() {
 66 |                 child.onComplete();
 67 |             }
 68 |              
 69 |         });
 70 |     }
 71 | }
 72 | ~~~
 73 | 
 74 | 这一次我们不需要管理资源,但我们需要处理 `Subscription` 切换:
 75 | 
 76 | 1. 首先我们创建一个空的 arbiter,把它设置给 child。
 77 | 2. 如果用户的工厂方法抛出了异常,我们就可以安全地给 child 发送 onError 了,因为它已经有了 Subscription(尽管是个 arbiter)。
 78 | 3. 我们不能直接订阅到 child(因为它已经有了 Subscription),所以我们创建一个新的 Subscriber,并重写 `onSubscribe` 方法。
 79 | 4. 我们把“真正的” Subscription 设置给 arbiter,其他的事件直接转发给 child 即可。
 80 | 
 81 | ## 延迟一段时间后订阅
 82 | 
 83 | 让我们看看现在的 `delaySubscription()` 操作符的实现,它把实际订阅操作延迟了一定的时间。我们几乎可以把已有的实现代码拷贝过来,但由于 API 发生了变化,会发生编译问题:
 84 | 
 85 | ~~~ java
 86 | public final class OnSubscribeDelayTimed
 87 | implements OnSubscribe {
 88 |     final Publisher source;
 89 |     final Scheduler scheduler;
 90 |     final long delay;
 91 |     final TimeUnit unit;
 92 |     public OnSubscribeDelayTimed(
 93 |             Publisher source, 
 94 |             long delay, TimeUnit unit, 
 95 |             Scheduler scheduler) {
 96 |         this.source = source;
 97 |         this.delay = delay;
 98 |         this.unit = unit;
 99 |         this.scheduler = scheduler;
100 |     }
101 |     @Override
102 |     public void call(Subscriber child) {
103 |         Scheduler.Worker w = scheduler.createWorker();
104 |          
105 |         // child.add(w);
106 |          
107 |         w.schedule(() -> {
108 |             // if (!child.isUnsubscribed()) {
109 |  
110 |                 source.subscribe(child);
111 |  
112 |             // }
113 |         }, delay, unit);
114 |     }
115 | }
116 | ~~~
117 | 
118 | 我们不能直接把资源添加到 child 中,也不能检查它是否已经被取消订阅(RS 中没有这两个方法了)。为了能清除 worker,我们就需要一个 disposable 容器,为了能够取消订阅,我们还需要一个可以“重放”数据源提供的 `Subscription` 取消操作的东西:
119 | 
120 | ~~~ java
121 | @Override
122 | public void call(Subscriber child) {
123 |     Scheduler.Worker w = scheduler.createWorker();
124 |      
125 |     SubscriptionArbiter sa = new SubscriptionArbiter();    // (1)
126 |  
127 |     DisposableSubscription dsub = 
128 |         new DisposableSubscription(
129 |             sa, new DisposableList());                     // (2)
130 |      
131 |     dsub.add(w);                                           // (3)
132 |      
133 |     child.onSubscribe(dsub);                               // (4)
134 |      
135 |     w.schedule(() -> {
136 |         source.subscribe(new Subscriber() {             // (5)
137 |             @Override
138 |             public void onSubscribe(Subscription s) {
139 |                 sa.setSubscription(s);                     // (6)
140 |             }
141 |  
142 |             @Override
143 |             public void onNext(T t) {
144 |                 child.onNext(t);
145 |             }
146 |  
147 |             @Override
148 |             public void onError(Throwable t) {
149 |                 child.onError(t);
150 |             }
151 |  
152 |             @Override
153 |             public void onComplete() {
154 |                 child.onComplete();
155 |             }
156 |              
157 |         });
158 |     }, delay, unit);
159 | ~~~
160 | 
161 | 比原来的实现多了很多代码,但值得这样做:
162 | 
163 | 1. 我们需要一个 `SubscriptionArbiter` 来进行占位,因为实际的 `Subscription` 会延迟到来,所以我们需要用 arbiter 先记录下取消操作。
164 | 2. 如果 child 要取消整个操作,那我们就需要取消这一次调度(直接取消这个 worker)。但由于 arbiter 没有资源管理能力,所以我们需要一个 disposable 容器。当然,我们只有一个资源,用不着一个 List,所以你可以实现一个自己的单一 disposable 容器类。
165 | 3. 我们把 worker 加入到容器中,它就会替我们处理取消的事宜了。
166 | 4. 当我们设置好 arbiter 和 disposable 之后,我们就可以把它们交给 child 了。然后 child 就可以随意进行 `request()` 以及 `cancel()` 操作了,arbiter 和 disposable 会在合适的时机(_实际 Subscription 到来时_),把记录下来的操作都转发给数据源/资源了。
167 | 5. 由于我们已经给 child 设置过了 Subscription,所以我们不能在调度时直接使用 child。我们创建一个 wrapper,在收到 Subscription 时设置给 arbiter,并且转发其他的 onXXX 事件。
168 | 6. 我们在把实际的 Subscription 设置给 arbiter 时,arbiter 会重放积累的 request/cancel 操作。
169 | 
170 | 现在你可能觉得最终调度时的 `Subscriber`(5)有错误,它没有在 `onNext` 中调用 `sa.produced(1)`。这确实会导致请求量计数不一致,但是一旦实际的 `Subscription` 被设置之后,后续 child 的 `request(n)` 都会原封不动地转发给上游,而我们后面又不会再调用 `setSubscription()` 了。所以上游能收到正确的请求量,即便 arbiter 计数不一致,也不会导致任何问题。为了保证更安全,你可以:
171 | 
172 | + 在 `onNext` 中调用 `sa.produced(1)`;
173 | + 或者实现一个自己的 arbiter,只接受一个 `Subscription`,并且在收到它之后停止计数。
174 | 
175 | ## 总结
176 | 
177 | 在本文中,我展示了两种使用 `SubscriptionArbiter` 的场景。幸运的是,不是所有的操作符都需要进行这样的操作,但主流的都需要,因此在处理“实际” `Subscription` 延迟抵达以及 `cancel()` 的同时释放资源的问题时,我们需要两种特殊的 `Subscription`(_`SubscriptionArbiter` 和 `DisposableSubscription`_)
178 | 
179 | 由于这种情况会频繁出现,我相信 RxJava 2.0 会提供一种标准而且高效的实现方案,来帮助我们遵循 Reactive-Streams 规范。
180 | 
181 | 作为 RS API 系列的总结,RS 最小的接口集合就能满足典型异步数据流场景的需求了。我们可以把 RxJava 的思想迁移到 RS 中,但基础设施以及一些辅助工具都需要从头开始实现。我已经展示了几种基本的 `Subscription`:`SingeSubscription`,`SingleDelayedSubscription`,`SubscriptionArbiter` 以及 `DisposableSubscription`。再加上一些其他类似的工具类,它们将是实现 RxJava 2.0 操作符的主要工具类。
182 | 
183 | 在下一个系列中,我将讲解响应式编程中争议最多的一个类型(_`Subject`_),而且我不仅会讲到它们的细节,还会讲如何实现我们自己的变体。
184 | 


--------------------------------------------------------------------------------
/css/fastclick.min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";function t(e,o){function i(t,e){return function(){return t.apply(e,arguments)}}var r;if(o=o||{},this.trackingClick=!1,this.trackingClickStart=0,this.targetElement=null,this.touchStartX=0,this.touchStartY=0,this.lastTouchIdentifier=0,this.touchBoundary=o.touchBoundary||10,this.layer=e,this.tapDelay=o.tapDelay||200,this.tapTimeout=o.tapTimeout||700,!t.notNeeded(e)){for(var a=["onMouse","onClick","onTouchStart","onTouchMove","onTouchEnd","onTouchCancel"],c=this,s=0,u=a.length;u>s;s++)c[a[s]]=i(c[a[s]],c);n&&(e.addEventListener("mouseover",this.onMouse,!0),e.addEventListener("mousedown",this.onMouse,!0),e.addEventListener("mouseup",this.onMouse,!0)),e.addEventListener("click",this.onClick,!0),e.addEventListener("touchstart",this.onTouchStart,!1),e.addEventListener("touchmove",this.onTouchMove,!1),e.addEventListener("touchend",this.onTouchEnd,!1),e.addEventListener("touchcancel",this.onTouchCancel,!1),Event.prototype.stopImmediatePropagation||(e.removeEventListener=function(t,n,o){var i=Node.prototype.removeEventListener;"click"===t?i.call(e,t,n.hijacked||n,o):i.call(e,t,n,o)},e.addEventListener=function(t,n,o){var i=Node.prototype.addEventListener;"click"===t?i.call(e,t,n.hijacked||(n.hijacked=function(t){t.propagationStopped||n(t)}),o):i.call(e,t,n,o)}),"function"==typeof e.onclick&&(r=e.onclick,e.addEventListener("click",function(t){r(t)},!1),e.onclick=null)}}var e=navigator.userAgent.indexOf("Windows Phone")>=0,n=navigator.userAgent.indexOf("Android")>0&&!e,o=/iP(ad|hone|od)/.test(navigator.userAgent)&&!e,i=o&&/OS 4_\d(_\d)?/.test(navigator.userAgent),r=o&&/OS [6-7]_\d/.test(navigator.userAgent),a=navigator.userAgent.indexOf("BB10")>0;t.prototype.needsClick=function(t){switch(t.nodeName.toLowerCase()){case"button":case"select":case"textarea":if(t.disabled)return!0;break;case"input":if(o&&"file"===t.type||t.disabled)return!0;break;case"label":case"iframe":case"video":return!0}return/\bneedsclick\b/.test(t.className)},t.prototype.needsFocus=function(t){switch(t.nodeName.toLowerCase()){case"textarea":return!0;case"select":return!n;case"input":switch(t.type){case"button":case"checkbox":case"file":case"image":case"radio":case"submit":return!1}return!t.disabled&&!t.readOnly;default:return/\bneedsfocus\b/.test(t.className)}},t.prototype.sendClick=function(t,e){var n,o;document.activeElement&&document.activeElement!==t&&document.activeElement.blur(),o=e.changedTouches[0],n=document.createEvent("MouseEvents"),n.initMouseEvent(this.determineEventType(t),!0,!0,window,1,o.screenX,o.screenY,o.clientX,o.clientY,!1,!1,!1,!1,0,null),n.forwardedTouchEvent=!0,t.dispatchEvent(n)},t.prototype.determineEventType=function(t){return n&&"select"===t.tagName.toLowerCase()?"mousedown":"click"},t.prototype.focus=function(t){var e;o&&t.setSelectionRange&&0!==t.type.indexOf("date")&&"time"!==t.type&&"month"!==t.type?(e=t.value.length,t.setSelectionRange(e,e)):t.focus()},t.prototype.updateScrollParent=function(t){var e,n;if(e=t.fastClickScrollParent,!e||!e.contains(t)){n=t;do{if(n.scrollHeight>n.offsetHeight){e=n,t.fastClickScrollParent=n;break}n=n.parentElement}while(n)}e&&(e.fastClickLastScrollTop=e.scrollTop)},t.prototype.getTargetElementFromEventTarget=function(t){return t.nodeType===Node.TEXT_NODE?t.parentNode:t},t.prototype.onTouchStart=function(t){var e,n,r;if(t.targetTouches.length>1)return!0;if(e=this.getTargetElementFromEventTarget(t.target),n=t.targetTouches[0],o){if(r=window.getSelection(),r.rangeCount&&!r.isCollapsed)return!0;if(!i){if(n.identifier&&n.identifier===this.lastTouchIdentifier)return t.preventDefault(),!1;this.lastTouchIdentifier=n.identifier,this.updateScrollParent(e)}}return this.trackingClick=!0,this.trackingClickStart=t.timeStamp,this.targetElement=e,this.touchStartX=n.pageX,this.touchStartY=n.pageY,t.timeStamp-this.lastClickTimen||Math.abs(e.pageY-this.touchStartY)>n?!0:!1},t.prototype.onTouchMove=function(t){return this.trackingClick?((this.targetElement!==this.getTargetElementFromEventTarget(t.target)||this.touchHasMoved(t))&&(this.trackingClick=!1,this.targetElement=null),!0):!0},t.prototype.findControl=function(t){return void 0!==t.control?t.control:t.htmlFor?document.getElementById(t.htmlFor):t.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")},t.prototype.onTouchEnd=function(t){var e,a,c,s,u,l=this.targetElement;if(!this.trackingClick)return!0;if(t.timeStamp-this.lastClickTimethis.tapTimeout)return!0;if(this.cancelNextClick=!1,this.lastClickTime=t.timeStamp,a=this.trackingClickStart,this.trackingClick=!1,this.trackingClickStart=0,r&&(u=t.changedTouches[0],l=document.elementFromPoint(u.pageX-window.pageXOffset,u.pageY-window.pageYOffset)||l,l.fastClickScrollParent=this.targetElement.fastClickScrollParent),c=l.tagName.toLowerCase(),"label"===c){if(e=this.findControl(l)){if(this.focus(l),n)return!1;l=e}}else if(this.needsFocus(l))return t.timeStamp-a>100||o&&window.top!==window&&"input"===c?(this.targetElement=null,!1):(this.focus(l),this.sendClick(l,t),o&&"select"===c||(this.targetElement=null,t.preventDefault()),!1);return o&&!i&&(s=l.fastClickScrollParent,s&&s.fastClickLastScrollTop!==s.scrollTop)?!0:(this.needsClick(l)||(t.preventDefault(),this.sendClick(l,t)),!1)},t.prototype.onTouchCancel=function(){this.trackingClick=!1,this.targetElement=null},t.prototype.onMouse=function(t){return this.targetElement?t.forwardedTouchEvent?!0:t.cancelable&&(!this.needsClick(this.targetElement)||this.cancelNextClick)?(t.stopImmediatePropagation?t.stopImmediatePropagation():t.propagationStopped=!0,t.stopPropagation(),t.preventDefault(),!1):!0:!0},t.prototype.onClick=function(t){var e;return this.trackingClick?(this.targetElement=null,this.trackingClick=!1,!0):"submit"===t.target.type&&0===t.detail?!0:(e=this.onMouse(t),e||(this.targetElement=null),e)},t.prototype.destroy=function(){var t=this.layer;n&&(t.removeEventListener("mouseover",this.onMouse,!0),t.removeEventListener("mousedown",this.onMouse,!0),t.removeEventListener("mouseup",this.onMouse,!0)),t.removeEventListener("click",this.onClick,!0),t.removeEventListener("touchstart",this.onTouchStart,!1),t.removeEventListener("touchmove",this.onTouchMove,!1),t.removeEventListener("touchend",this.onTouchEnd,!1),t.removeEventListener("touchcancel",this.onTouchCancel,!1)},t.notNeeded=function(t){var e,o,i,r;if("undefined"==typeof window.ontouchstart)return!0;if(o=+(/Chrome\/([0-9]+)/.exec(navigator.userAgent)||[,0])[1]){if(!n)return!0;if(e=document.querySelector("meta[name=viewport]")){if(-1!==e.content.indexOf("user-scalable=no"))return!0;if(o>31&&document.documentElement.scrollWidth<=window.outerWidth)return!0}}if(a&&(i=navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/),i[1]>=10&&i[2]>=3&&(e=document.querySelector("meta[name=viewport]")))){if(-1!==e.content.indexOf("user-scalable=no"))return!0;if(document.documentElement.scrollWidth<=window.outerWidth)return!0}return"none"===t.style.msTouchAction||"manipulation"===t.style.touchAction?!0:(r=+(/Firefox\/([0-9]+)/.exec(navigator.userAgent)||[,0])[1],r>=27&&(e=document.querySelector("meta[name=viewport]"),e&&(-1!==e.content.indexOf("user-scalable=no")||document.documentElement.scrollWidth<=window.outerWidth))?!0:"none"===t.style.touchAction||"manipulation"===t.style.touchAction?!0:!1)},t.attach=function(e,n){return new t(e,n)},"function"==typeof define&&"object"==typeof define.amd&&define.amd?define(function(){return t}):"undefined"!=typeof module&&module.exports?(module.exports=t.attach,module.exports.FastClick=t):window.FastClick=t}();


--------------------------------------------------------------------------------
/_posts/2016-08-05-schedulers-1.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: 调度器 Scheduler(一):实现自定义 Scheduler
  4 | tags:
  5 |     - Scheduler
  6 | ---
  7 | 
  8 | 原文 [Schedulers (part 1)](http://akarnokd.blogspot.com/2015/05/schedulers-part-1.html){:target="_blank"}
  9 | 
 10 | ## 介绍
 11 | 
 12 | `Scheduler` 是 RxJava 异步和并行计算的关键。尽管已经存在了很多标准的 scheduler,并且你也可以把一个 `Executor` 包装成一个 scheduler,我们还是应该理解 scheduler 是如何一步一步实现的,以便我们利用其他的并发资源,例如 GUI 框架的消息循环机制。
 13 | 
 14 | ## Scheduler API
 15 | 
 16 | 如果你对 Rx.NET 的 `IScheduler` API 比较熟悉的话,你就会发现 RxJava 的 scheduler API 稍有不同。它们的不同主要体现在处理递归调度的问题上(recursive scheduling problem)。Rx.NET 的解决方法是把实际的 scheduler 注入到被调度的任务中。
 17 | 
 18 | RxJava 则是借鉴了 `Iterable`/`Iterator` 模式的思想,定义了一套 `Scheduler`/`Worker` API。RxJava 的 scheduler 不进行任何调度的工作,但它负责创建 `Worker`,worker 负责实际调度,无论是直接调度还是递归调度。此外 `Worker` 还实现了 `Subscription` 接口,所以它可以被取消订阅,这会取消所有还未执行的任务,此后也不会再接受新的任务(尽可能)。这对于操作符(例如重复任务)使用 scheduler 非常有用,如果下游取消订阅了整个链条,就能一次取消所有定时的任务。
 19 | 
 20 | `Scheduler`/`Worker` 需要满足以下的要求:
 21 | 
 22 | 1. 所有的方法都需要是线程安全的;
 23 | 2. `Worker` 需要保证即时、串行提交的任务按照先进先出(FIFO)的顺序被执行;
 24 | 3. `Worker` 需要尽可能保证被取消订阅时要取消还未执行的任务;
 25 | 4. 取消订阅一个 `Worker` 不能影响同一个 Scheduler 的其他 Worker;
 26 | 
 27 | 这些要求看起来比较严格,但这让我们对并发数据流的推算更加容易,这和我们严格要求 Observer 的方法被串行调用是一样的(_译者注:这一点不清楚的朋友可以看一下[本系列的第一篇文章的介绍部分](/AdvancedRxJava/2016/05/06/operator-concurrency-primitives/#section){:target="_blank"}_)。
 28 | 
 29 | 除了上面必须的要求,下面几点特性如果能具备也是非常好的:
 30 | 
 31 | 1. 一个被 Worker 调度的任务最好不要切换线程执行(hopping threads),保证在一个任务只在一个线程内执行能提升性能(避免线程切换的开销)。
 32 | 2. 串行发起的延迟任务,如果延迟时间相同,最好也能按照 FIFO 的顺序执行,并发调度的任务不做此要求。
 33 | 
 34 | 考虑到上面的这些要求,一个保守的 scheduler 实现最好用单线程的线程池来支持每个 worker,而这也正是标准 scheduler 的实现方案:底层的 `ScheduledExecutorService` 保证了上面的特性。
 35 | 
 36 | ## 实现一个自定义的 scheduler
 37 | 
 38 | 假设我们需要实现的 scheduler 具备以下属性:(1)只能有一个 worker 线程;(2)一个线程局部的上下文信息要能够在不同的 worker 之间传递,且能被正在执行的任务访问到。
 39 | 
 40 | 显然,如果我们只有第一个要求,那我们可以直接利用 `Schedulers.from()` 包装一个单线程的 `Executor`,但第二个要求需要我们在一个任务被调度和执行的时候做一些额外的操作。
 41 | 
 42 | 为了完成我们的需求,我会复用一些 RxJava 自己的 scheduler 原语:`ScheduledAction` 和 `NewThreadWorker`。(_注意,这些内部类都是随时可能发生变动的,这里我使用它们是为了避免考虑它们负责的一些细节,让我可以聚焦于我们创建 scheduler 的逻辑_)
 43 | 
 44 | 我们先看一下类的结构:
 45 | 
 46 | ~~~ java
 47 | public final class ContextAwareScheduler 
 48 | extends Scheduler {
 49 |     
 50 |     public static final ContextAwareScheduler INSTANCE = 
 51 |             new ContextAwareScheduler();                       // (1)
 52 |     
 53 |     final NewThreadWorker worker;
 54 |     
 55 |     private ContextAwareScheduler() {
 56 |         this.worker = new NewThreadWorker(
 57 |                 new RxThreadFactory("ContextAwareScheduler")); // (2)
 58 |     }
 59 |     @Override
 60 |     public Worker createWorker() {
 61 |         return new ContextAwareWorker(worker);                 // (3)
 62 |     }
 63 |     
 64 |     static final class ContextAwareWorker extends Worker {
 65 | 
 66 |         final CompositeSubscription tracking;                  // (4)
 67 |         final NewThreadWorker worker;
 68 | 
 69 |         public ContextAwareWorker(NewThreadWorker worker) {
 70 |             this.worker = worker;
 71 |             this.tracking = new CompositeSubscription();
 72 |         }
 73 |         @Override
 74 |         public Subscription schedule(Action0 action) {
 75 |             // implement
 76 |         }
 77 |         @Override
 78 |         public Subscription schedule(Action0 action, 
 79 |                 long delayTime, TimeUnit unit) {
 80 |             // implement
 81 |         }
 82 |         @Override
 83 |         public boolean isUnsubscribed() {
 84 |             return tracking.isUnsubscribed();                  // (5)
 85 |         }
 86 |         @Override
 87 |         public void unsubscribe() {
 88 |             tracking.unsubscribe();
 89 |         }
 90 |     }
 91 | }
 92 | ~~~
 93 | 
 94 | 我们的 `ContextAwareScheduler` 看起来可能有点吓人,但别怕,它的逻辑还是很直观的:
 95 | 
 96 | 1. 由于我们只允许全局的单一线程,所以我们的 scheduler 不能存在多个实例,因此我们使用了一个静态的单例。
 97 | 2. 我们的 scheduler 会把几乎所有的工作都转交给内部的一个 worker。我们复用 `NewThreadWorker` 和 `RxThreadFactory` 类来为我们的 worker 实例提供一个单一的后台线程。
 98 | 3. 为了满足我们的第四个必须要求,我们不能对外也只提供一个 worker 实例,否则一旦 worker 被取消订阅,所有人的 worker 都被取消订阅了。
 99 | 4. 为了满足第三个必须要求,我们需要单独为每个 worker 实例记录提交过来的任务。
100 | 5. 记录这些任务让我们可以检查是否已经取消订阅,以及进行取消订阅操作。
101 | 
102 | 接下来,我们就需要前面提到的线程局部上下文了:
103 | 
104 | ~~~ java
105 | public final class ContextManager {
106 |     static final ThreadLocal ctx = new ThreadLocal<>();
107 |     
108 |     private ContextManager() {
109 |         throw new IllegalStateException();
110 |     }
111 |     
112 |     public static Object get() {
113 |         return ctx.get();
114 |     }
115 |     public static void set(Object context) {
116 |         ctx.set(context);
117 |     }
118 | }
119 | ~~~
120 | 
121 | `ContextManager` 仅仅是包装了一个静态的 `ThreadLocal` 变量。在实际情况中,你可能要把 `Object` 替换成你需要的类型。
122 | 
123 | 现在让我们继续看 `schedule()` 的实现:
124 | 
125 | ~~~ java
126 | // ...
127 | @Override
128 | public Subscription schedule(Action0 action) {
129 |     return schedule(action, 0, null);               // (1)
130 | }
131 | @Override
132 | public Subscription schedule(Action0 action, 
133 |         long delayTime, TimeUnit unit) {
134 | 
135 |     if (isUnsubscribed()) {                         // (2)
136 |         return Subscriptions.unsubscribed();
137 |     }
138 |     
139 |     Object context = ContextManager.get();          // (3)
140 |     Action0 a = () -> {
141 |         ContextManager.set(context);                // (4)
142 |         action.call();
143 |     };
144 |     
145 |     return worker.scheduleActual(a, 
146 |         delayTime, unit, tracking);                 // (5)
147 | }
148 | // ...
149 | ~~~
150 | 
151 | 非常简洁!
152 | 
153 | 1. 我们把即时的调度作为延迟为 0 的延迟调度。所有底层的实现都会正确解读这个 0 延迟,并且实现要求的 FIFO 顺序。
154 | 2. 如果当前的 worker 已经被取消订阅,那我们无需做任何事情,我们直接返回一个代表已经取消订阅的常量。注意,取消订阅总是需要耗费一点时间的,那就可能存在一个竞争窗口,在这个时间窗口内,可能会有一些任务(或者事件)仍然能被调度。这一竞争的处理是在 `scheduleActual` 函数中,我们后面会展开。
155 | 3. 我们获取当前线程的线程局部上下文,并把任务包装到一个新的任务中。
156 | 4. 在新的任务被执行时,我们把之前获取的局部线程上下文保存到新任务被执行的线程中。由于我们的 worker 底层都是同一个线程,所以在一个任务正在执行期间,它的线程局部上下文不会被后面的任务覆盖。
157 | 5. 最后,我们把包装好的新任务以及延迟信息委托到底层的 `NewThreadWorker` 中。由于我们把 `tracking` 传了进去,所以当任务执行完毕后,任务会被正确地移除,被取消订阅,或者和 worker 一起被取消订阅。
158 | 
159 | 正如我们在上面的解释中提到的,第二步天生和 worker 的取消订阅存在竞争,但我们不能把竞争窗口内的任务丢下不管(我们需要接受它,让它被执行,或者立即取消它)。这里我们 subscription 容器类的取消订阅保证就发挥作用了(_译者注:这一点不清楚的朋友可以看一下 [subscription 容器类的第一篇文章的介绍部分](/AdvancedRxJava/2016/07/15/operator-concurrency-primitives-subscription-containers-1/#section){:target="_blank"}_)。如果我们把底层线程池返回的 `Future` 对象包装为一个 `Subscription`,那我们就可以安全地把它加到 `tracking` 容器中,它会被正确保存或者被立即取消订阅。
160 | 
161 | 让我们尝试一下:
162 | 
163 | ~~~ java
164 | Worker w = INSTANCE.createWorker();
165 | 
166 | CountDownLatch cdl = new CountDownLatch(1);
167 | 
168 | ContextManager.set(1);
169 | w.schedule(() -> {
170 |     System.out.println(Thread.currentThread());
171 |     System.out.println(ContextManager.get());
172 | });
173 | 
174 | ContextManager.set(2);
175 | w.schedule(() -> {
176 |     System.out.println(Thread.currentThread());
177 |     System.out.println(ContextManager.get());
178 |     cdl.countDown();
179 | });
180 | 
181 | cdl.await();
182 | 
183 | ContextManager.set(3);
184 | 
185 | Observable.timer(500, TimeUnit.MILLISECONDS, INSTANCE)
186 | .doOnNext(v -> {
187 |     System.out.println(Thread.currentThread());
188 |     System.out.println(ContextManager.get());
189 | }).toBlocking().first();
190 | 
191 | w.unsubscribe();
192 | ~~~
193 | 
194 | _译者注,运行输出如下:_
195 | 
196 | ~~~ bash
197 | Thread[ContextAwareScheduler1,5,main]
198 | 1
199 | Thread[ContextAwareScheduler1,5,main]
200 | 2
201 | Thread[ContextAwareScheduler1,5,main]
202 | 3
203 | ~~~
204 | 
205 | ## 总结
206 | 
207 | Scheduler API 让我们有机会指定与 Observable 链条操作相关的任务在何地、甚至何时被执行。内置的标准 scheduler 应该能满足大部分并发的需求,但还是存在一些场景,我们需要使用和实现自定义的 scheduler。在本文中,我展示了如何利用 RxJava 已有的一些类来实现具有自定义逻辑的自定义 scheduler。
208 | 
209 | 在接下来的文章中,我们会深入介绍 `ScheduledAction` 的原理,并且借助它的一些概念,让我们在调度任务时进行更多的控制,例如和线程中断协调工作。
210 | 


--------------------------------------------------------------------------------
/_posts/2016-09-10-the-reactive-streams-api-part-1.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: Reactive-Streams API(一):概览
  4 | tags:
  5 |     - Reactive Stream
  6 | ---
  7 | 
  8 | 原文 [The Reactive-Streams API (part 1)](http://akarnokd.blogspot.com/2015/06/the-reactive-streams-api-part-1.html){:target="_blank"}
  9 | 
 10 | ## 介绍
 11 | 
 12 | 在我(_原作者_)个人看来,[Reactive-Streams API](https://github.com/reactive-streams/reactive-streams-jvm/index.html){:target="_blank"} 是目前描述异步、取消、支持 backpressure 数据流时最优雅的 API 了,它受到了 RxJava 很多影响,替换了由 Erik Meijer 提出的 `IObservable`/`IObserver` 设计。
 13 | 
 14 | 回到 4 月份,我花了一周的时间,尝试把 RxJava 1.x 移植到新的 API 上来。一开始,最大的障碍是新的 `Subscriber` 接口没有资源管理的途径,以及如何把 RxJava 的 `Producer` 和 `Subscription` 合并为新的 `Subscription` 接口。然而,当我开始实现一系列基础支持的类之后,我开始觉得新的 API 简直设计得太巧妙了,而这一设计最重要的收获应该就是 backpressure 的支持一下子变得直截了当了。
 15 | 
 16 | 带着新的见解,我回过头来看 RxJava 的一些操作符以及 `Producer` 的实现,我突然可以轻易发现一些 bug 以及优化空间了,更不用提后来编写的这一系列讲解 RxJava 工作原理的文章了。
 17 | 
 18 | 在这个小的系列中,我将介绍新的 reactive-streams API,以及它与 RxJava 中一些基础结构的关系。
 19 | 
 20 | ## 包的组织,以及命名上的困惑
 21 | 
 22 | 熟悉了 RxJava 之后,面对 reactive-streams(RS)的第一头拦路虎应该就是同样的概念被取了不同的名字。
 23 | 
 24 | 和 `rx.Observable` 对应的是 `org.reactivestreams.Publisher`,而且它是一个接口。由于 Java 不支持而且很可能也不会支持扩展方法,所以用一个 `Publisher` 接口返回数据源,对于在它上面应用一系列操作并没有什么帮助。幸运的是,通过继承新的接口,目前 RxJava 已有的实现都可以保留。
 25 | 
 26 | 用于取消的 `rx.Subscription` 和 backpressure 控制的 `rx.Producer` 现在被合并为了 `org.reactivestreams.Subscription` 接口。而且取消的接口也从 `unsubscribe()` 变成了 `cancel()`,而且也没有(也几乎不需要)检查是否已经取消的 `isUnsubscribed()` 接口了。由于 RS 没有规定任何资源管理的方式,我们需要把 `cancel()` 方法包装到一个资源管理的对象中,使得我们可以在容器中使用它。由于这一命名的困惑,RxJava 2.0 很可能会把资源管理相关的类改成 C# 风格的 `Disposable` 接口。
 27 | 
 28 | RS 中没有一个和 `rx.Observer` 对应的存在,但 `org.reactivestreams.Subscriber` 和 `rx.Subscriber` 比较类似。新的 Subscriber 也不能直接被取消,它将接受一个控制对象,用来执行取消操作,或者请求更多操作。此外,它也不再保留 `onStart()` 和 `setProducer()` 这两个方法,这两个方法在 RxJava 中常常导致大家不知道该怎么启动一个 `Subscriber`,它只会有一个接收 `Subscription` 的 `onSubscribe()` 函数。前面的方式会调用 `rx.Subscriber.request(n)` 函数,而后面的方式则需要保存 `Subscription` 对象,并调用 `org.reactivestreams.Subscription.request(n)`。
 29 | 
 30 | 还有人对终止信号的函数名称存在一些争议,RS 中它叫做 `onComplete()`,而不是 RxJava 以及 Rx.NET 中的 `onCompleted()`。希望 IDE 自动补全的功能可以减少我们在这一变化上的问题。
 31 | 
 32 | 最后,RS 定义了一个名为 `Processor` 的接口,它继承了 `org.reactivestreams.Publisher` 和 `org.reactivestreams.Subscriber`,它和 RxJava 中的 `Subject` 一样。由于 `Processor` 是一个接口,所以对 `rx.Subject` 唯一的更改就是实现这个接口。
 33 | 
 34 | ## Java 9+ Flow
 35 | 
 36 | RxJava 和 RS 之间还是有些不一样的,但你可能听说了 Doug Lea 希望把 reactive-streams 的思想引入 Java 9,利用一个叫做 `java.util.concurrent.Flow` 的容器类。名字和 RS 一样,但包名不一样。我对此一直持怀疑态度:
 37 | 
 38 | + 这个“通用的平台”并没有那么通用:只有 Java 9+ 的用户才能使用。RS 对 Java 6 兼容以及安卓平台友好,是一种更好的选择。绝大多数项目都引入了很多第三方库,不差多 2 个(RS + RxJava 2.0)。
 39 | + Java 9 不会在内部使用这套 API。目前没有看到任何基于 Observer 模式的 NIO,网络,文件 I/O,以及 UI 事件系统的改进计划。更何况,如果没有流畅的 API 支持,绝大部分用户依然需要额外的依赖才能使用。通过对老版本的 JDK 发布一个包含 Flow 的更新,可以缓解这一情况。但是,没有方法扩展的话,用户仍然不得不依赖额外的库,以及实例包装。
 40 | + 我并不确定响应式编程已经确定了它的最终形态:例如,Applied Duality 的 `AsyncObservable` 是用来捕获老的 `onXXX` 事件的,将来有一天可能会被合入 RS 2.0(尽管我不认为有这样的需求)。把依赖改成 RS 2.0 和 RxJava 3.0 也许比改变 JDK 的 Flow API 更加简单。这里有一个历史教训,Rx.NET 把 `IObservable`/`IObserver` 加入到了 BCL 中,我认为这是给自己挖了个坑,如果想要加入 RS 阵营,他们就需要想出怎么让新老接口能一起保留,就像一个集合需要考虑前置泛型和后置泛型一样(pre-generics and post-generics collections)。注意这里我假设他们接受 RS 作为 Rx.NET 的基础,而不是再发明一套新的东西。
 41 | 
 42 | ## 对 RS 的一些评价
 43 | 
 44 | 我对 RS 新的 4 个接口非常满意,但其文字规范有点差强人意。我认为它太过严格,而且会妨碍一些很明智的实现方案。
 45 | 
 46 | ### 只允许 NullPointerException
 47 | 
 48 | 尽管函数允许在传入参数是 null 时抛出 NPE,但是各个模块可能会处于非法的状态,或者 `Subscription.request()` 会被传入非法的负参数。目前 RS 1.0 的规范不允许按照大多数 Java API 的方式处理这些情况,即抛出 `IllegalArgumentException` 和 `IllegalStateException`。
 49 | 
 50 | 有人可能会说可以利用 `onError()` 来发出这些异常,但我会展示一些特殊情况,如果我们严格遵循 RS 规范,我们就无法处理。
 51 | 
 52 | 假设我们有一个 `Subscriber`,而它错误地订阅了两个不同的 `Publisher`。规范中定义,这种情况是不允许的,应该在 `onSubscribe()` 中拒绝第二个 `Subscription`:
 53 | 
 54 | ~~~ java
 55 | // ...
 56 | Subscription subscription;
 57 | @Override
 58 | public void onSubscribe(Subscription s) {
 59 |     if (subscription != null) {
 60 |         s.cancel();
 61 |         onError(new IllegalStateException("§x.y: ..."));
 62 |         return;
 63 |     }
 64 |     this.subscription = s;
 65 |     s.request(Long.MAX_VALUE);
 66 | }
 67 | ~~~
 68 | 
 69 | 看起来没什么问题,但是第一个 `Publisher` 是异步的,这就会导致两个问题:
 70 | 
 71 | + 第二次 `onSubscribe()` 调用立即执行了 `onError()`,这就可能会和第一个 `Publisher` 的 `onNext()` 同时发生。而这是规范禁止的。
 72 | + 不能直接保存 `Subscription` 的引用,需要使用 `AtomicReference` 以避免竞争,二者又增加了 `Subscriber` 的实现开销。
 73 | 
 74 | 所以唯一可能的 `onSubscribe()` 安全实现方式只能像下面这样:
 75 | 
 76 | ~~~ java
 77 | // ...
 78 | final AtomicReference subscription = ...;
 79 | @Override
 80 | public void onSubscribe(Subscription s) {
 81 |     if (!subscription.compareAndSet(null, s)) {
 82 |         s.cancel();
 83 |         new IllegalStateException("§x.y: ...").printStackTrace();
 84 |         return;
 85 |     }
 86 |     s.request(Long.MAX_VALUE);
 87 | }
 88 | ~~~
 89 | 
 90 | 并且依赖打印的日志被监控,然后串行化调用 Subscriber 的 `onXXX()` 方法。
 91 | 
 92 | 但我觉得直接抛出 `IllegalArgumentException`,让 `subscribe()` 调用失败是最简单的办法。
 93 | 
 94 | ### 非法的请求数量
 95 | 
 96 | `Subscription.request()` 应该传入一个正数,否则就应该通过 `onError()` 通知 Subscriber:
 97 | 
 98 | ~~~ java
 99 | // ...
100 | @Override
101 | public void request(long n) {
102 |     if (n <= 0) {
103 |         subscriber.onError(new IllegalArgumentException("§x.y: ..."));
104 |         return;
105 |     }
106 |     // ...
107 | }
108 | ~~~
109 | 
110 | 同样,如果我们异步调用的 `request()`,例如 `observeOn()` 希望在通过 `onNext()` 发出数据的同时,补充输入队列。这时 `onError()` 和 `onNext()` 也可能并行发生,这也是禁止的。
111 | 
112 | 我的建议和处理 `IllegalStateException` 类似:我们只能打印日志,不能直接抛出异常。
113 | 
114 | ### `request()` 不能抛出 NPE
115 | 
116 | 即便 `onXXX()` 可以抛出 `NullPointerException`,但 `Subscription.request()` 不能。我们只能把异常“反弹”(_没懂啥意思_)给 `Subscriber`,而如果这也失败了,那就只能打印日志了。
117 | 
118 | ### `Processor` 必须要一个 `Subscription`
119 | 
120 | RS 规范严重倾向于 cold observable,而 cold observable 必须有 `Subscription`。然而像 RxJava 中 `Subject` 这样的 hot observable,在 `onNext()` 被调用之前是有可能没有 `Subscription` 的。
121 | 
122 | 例如,如果你调用 `Observable.subscribe(PublishSubject)`,用 `PublishSubject` 来多播(multicast)一个 `Observable`,`PublishSubject` 会得到一个 `Subscription`。但如果用 `PublishSubject` 来多播鼠标移动的事件,这时是没法取消(以及支持 backpressure)的,因此 `Subscription` 是不必要的。
123 | 
124 | 这时我们就不得不用一个假的 `Subscription`,或者让 `onSubscribe()` 的调用成为 `Processor` 的一个可选项。
125 | 
126 | ### `Processor` 必须支持 backpressure
127 | 
128 | backpressure 的支持对 `Processor` 来说有时是不可能的:例如鼠标移动事件,是无法支持 backpressure 的,因此 `Subject` 可能需要缓冲或者丢掉过多的数据。把它实现为 `AsyncSubject` 是很琐碎的,无需考虑,但是如果使用其他的类型,就不得不转化为某种 `ReplaySubject`(很自然地,`PublishSubject` 会在没有收到 subscriber 时丢掉未请求的数据,但是持续传递数据的保证在我看来是非常有价值的,无法被推翻)。
129 | 
130 | 如果要遵守这一规范,将给所有的 subscriber 带来很大的开销,数据必须经过队列,而且可能需要使用锁,使得处理受限于最慢的请求者。
131 | 
132 | 如果是我,我会放宽规范,让 backpressure  的支持成为 `Processor` 的一个可选项。
133 | 
134 | ### `Subscription` 将会变得更保守
135 | 
136 | 规范中建议,`Subscription` 在实现时可以假定并发只是少数情况。如果你还记得 RxJava 中 `Producer` 的要求,我提到的是线程安全和可重入安全。
137 | 
138 | 在 `Subscription.request()` 的实现中,可重入安全依然是非常关键的。它能避免不必要的递归,而且也能处理 `onNext()` 中在处理当前数据之前调用 `request()` 的情况,这种情况下又可能会触发 `onNext()`,然后就陷入了无限递归。
139 | 
140 | 线程安全的需求可以被以下链条的行为证明。假设我们有一个链条在主线程订阅,不使用 RxJava 的 scheduler,通过 `observeOn()` 改变接收数据的线程。数据的消费者持续调用 `request()`,而这些调用都会转发到生产数据的数据源那里(主线程)。这时,就会有两个线程处于或者进入 `request()` 函数(_how?_),而且没有相应的原子性、同步保证,则重入检查以及请求处理将会出问题。有人可能会认为 `observeOn()` 已经把对 `Subscription` 的访问串行化了,但由于数据源并没有在 `Scheduler` 中执行,这些串行化在下游看来是没有作用的,而从上游来看则是低效的。
141 | 
142 | 由于 Subscriber 不知道 `request()` 的调用特性,它就不得不变得很保守,利用我在前文中介绍 `Producer` 时提到的串行访问方式。很自然地,任何对 `cancel()` 的实现,都不得不利用一个 `volatile` 变量了。
143 | 
144 | ## 总结
145 | 
146 | 上面的这些考虑并不是在写这篇文章的时候产生的,我早就尝试告知 RS 的设计者们了。不幸的是,我的考虑并没有被他们接受,而我的代码也没能赢得辩论。我们可以无休止地讨论我提出的问题,但我更倾向于在这里用代码来证明我的观点。
147 | 
148 | 不管怎样,如果 RS 能进行以下几点修正,它将会是一个非常好的设计规范:
149 | 
150 | + 错误处理:引用一下最新的 Godzilla 电影,“Let them throw!(抛出去)”
151 | + `Processor` 可以选择在 `Subscriber` 端实现 backpressure 和取消。
152 | + 绝大多数 `Subscription` 都应该更保守一点,并且实现线程安全。
153 | 
154 | 在接下来的文章中,我将从 RxJava 中的两个简单 `Producer` 入手,`SingleProducer` 和 `SingleDelayedProducer`,展示如何把它们转换为 `SingleSubscription` 和 `SingleDelayedSubscription`。
155 | 


--------------------------------------------------------------------------------
/_posts/2016-09-02-schedulers-4.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: 调度器 Scheduler(四,完结):实现 GUI 系统的 Scheduler
  4 | tags:
  5 |     - Scheduler
  6 | ---
  7 | 
  8 | 原文 [Schedulers (part 4 - final)](http://akarnokd.blogspot.com/2015/06/schedulers-part-4-final.html){:target="_blank"}
  9 | 
 10 | ## 介绍
 11 | 
 12 | 在关于 `Scheduler` 的最后一篇文章(本文)中,我将讲讲如何在那些没有暴露出 `ExecutorService` 的系统中实现 scheduler,例如 GUI 的事件循环线程。
 13 | 
 14 | ## 不基于 `Executor` 服务的 scheduler
 15 | 
 16 | 在一些类似于 Java AWT 事件循环的调度系统中,并没有提供 `Future` 这样的 API,只有一个提交任务的方法。另一些系统可能会提供一些成对的提交/移除任务的方法。
 17 | 
 18 | 假设我们在一些 GUI 事件循环系统中有如下的 API 可以提交任务和取消任务:
 19 | 
 20 | ~~~ java
 21 | public interface GuiEventLoop {
 22 |     void run(Runnable task);
 23 |     void cancel(Runnable task);
 24 | }
 25 |  
 26 | public static final class EDTEventLoop 
 27 | implements GuiEventLoop {
 28 |     @Override
 29 |     public void run(Runnable task) {
 30 |         SwingUtilities.invokeLater(task);
 31 |     }
 32 |  
 33 |     @Override
 34 |     public void cancel(Runnable task) {
 35 |         // not supported
 36 |     }
 37 | }
 38 | ~~~
 39 | 
 40 | 在这儿我定义了一个示例的 API,它的实现将会对 Java AWT 的 **事件分发线程(Event Dispatch Thread)**进行一个包装。不幸的是,EDT 不支持取消已经提交的任务,不过由于被提交的任务本来就不支持耗时操作,所以这一点对应用程序来说并不是大问题。
 41 | 
 42 | 我们会很自然地想到直接把上面的调用包装为一个 `Executor`:
 43 | 
 44 | ~~~ java
 45 | Executor exec = SwingUtilities::invokeLater;
 46 | ~~~
 47 | 
 48 | 然后利用[本系列第三篇](/AdvancedRxJava/2016/08/26/schedulers-3/index.html){:target="_blank"}中介绍的 `ExecutorScheduler`,但这种方式通常会带来一些额外的开销,而且我也会展示当我们可以通过 GUI 系统的某些方法取消、移除任务时,我们可以怎样处理这一问题。
 49 | 
 50 | 由于 GUI 的事件循环是单线程的,所以我们在实现 `Worker` 时无需考虑同步和中继问题,让我们看看更简单的 `GuiScheduler` 的类结构:
 51 | 
 52 | ~~~ java
 53 | public final class GuiScheduler extends Scheduler {
 54 |      
 55 |     final GuiEventLoop eventLoop;
 56 |      
 57 |     public GuiScheduler(GuiEventLoop el) {
 58 |         this.eventLoop = el;
 59 |     }
 60 |      
 61 |     @Override
 62 |     public Worker createWorker() {
 63 |         return new GuiWorker();
 64 |     }
 65 |  
 66 |     final class GuiWorker extends Worker {
 67 |         final CompositeSubscription tracking = 
 68 |             new CompositeSubscription();
 69 |         @Override
 70 |         public void unsubscribe() {
 71 |             tracking.unsubscribe();
 72 |         }
 73 |  
 74 |         @Override
 75 |         public boolean isUnsubscribed() {
 76 |             return tracking.isUnsubscribed();
 77 |         }
 78 |  
 79 |         @Override
 80 |         public Subscription schedule(Action0 action) {
 81 |             // implement
 82 |         }
 83 |  
 84 |         @Override
 85 |         public Subscription schedule(
 86 |                 Action0 action, 
 87 |                 long delayTime,
 88 |                 TimeUnit unit) {
 89 |             // implement
 90 |         }
 91 |     }
 92 | }
 93 | ~~~
 94 | 
 95 | 到目前为止还没有什么特殊之处:我们把所有的调度都转发到同一个 `GuiEventLoop` 实例中,并且单独记录被提交到每个 `Worker` 上的任务。由于我们假定 `GuiEventLoop` 是单线程的,所以就不必要实现队列漏逻辑了。下面我们首先看看非延迟的 `schedule()` 实现:
 96 | 
 97 | ~~~ java
 98 | @Override
 99 | public Subscription schedule(Action0 action) {
100 |     if (isUnsubscribed()) {                             // (1)
101 |         return Subscriptions.unsubscribed();
102 |     }
103 |     ScheduledAction sa = new ScheduledAction(action);
104 |     tracking.add(sa);
105 |     sa.add(Subscriptions.create(
106 |             () -> tracking.remove(sa)));                // (2)
107 |      
108 |     Runnable r = () -> {                                // (3)
109 |         if (!sa.isUnsubscribed()) {
110 |             sa.run();
111 |         }
112 |     };
113 |      
114 |     eventLoop.run(r);                                   // (4)
115 |      
116 |     sa.add(Subscriptions.create(
117 |             () -> eventLoop.cancel(r)));                // (5)
118 |      
119 |     return sa;
120 | }
121 | ~~~
122 | 
123 | 实现逻辑我们已经很熟悉了:
124 | 
125 | 1. 如果 worker 已经被取消订阅,我们就直接返回代表已取消订阅的 `Subscription` 实例。
126 | 2. 我们把任务包装为一个 `ScheduledAction`,并且实现记录和取消记录的逻辑。
127 | 3. 在这个例子中,`ScheduledAction` 是否已经被取消有必要区分,所以如果已经取消,那我们就不执行它。由于 `GuiEventLoop` 在取消任务时需要同一个 `Runnable` 实例,并且 `ScheduledAction.run()` 中也不检查 `isUnsubscribed()`,所以我们需要把执行和取消逻辑包装到一个 `Runnable` 中。
128 | 4. 我们把 Runnable 提交到 GuiEventLoop 中。
129 | 5. 然后我们在取消订阅时移除提交的任务。注意,如果我们交换(4)和(5),而且就在我们执行 `eventLoop.run(r)` 之前 worker 被取消订阅了,那我们就会立即移除 `r`(而此时 `r` 并不在 GuiEventLoop 中),那我们再执行 `eventLoop.run(r)` 时,就无法取消了。
130 | 
131 | 由于我们要适配的 API 不提供延迟执行的功能(延迟执行在处理定期执行时将很有用,例如动画),所以我们依然要利用[本系列第二篇](/AdvancedRxJava/2016/08/19/schedulers-2/index.html){:target="_blank"}中介绍的 `genericScheduler`:
132 | 
133 | ~~~ java
134 | @Override
135 | public Subscription schedule(
136 |         Action0 action, 
137 |         long delayTime,
138 |         TimeUnit unit) {
139 |  
140 |     if (delayTime <= 0) {                             // (1)
141 |         return schedule(action);
142 |     }
143 |     if (isUnsubscribed()) {                           // (2)
144 |         return Subscriptions.unsubscribed();
145 |     }
146 |     ScheduledAction sa = 
147 |             new ScheduledAction(action);
148 |     tracking.add(sa);
149 |     sa.add(Subscriptions.create(
150 |             () -> tracking.remove(sa)));              // (3)
151 |  
152 |     Future f = CustomWorker.genericScheduler       // (4)
153 |     .schedule(() -> {
154 |         Runnable r = () -> {
155 |             if (!sa.isUnsubscribed()) {
156 |                 sa.run();
157 |             }
158 |         };
159 |          
160 |         eventLoop.run(r);
161 |          
162 |         sa.add(Subscriptions.create(
163 |                 () -> eventLoop.cancel(r)));          // (5)
164 |          
165 |     }, delayTime, unit);
166 |  
167 |     sa.add(Subscriptions.create(
168 |             () -> f.cancel(false)));
169 |  
170 |     return sa;
171 | }
172 | ~~~
173 | 
174 | 这次我们延迟调度的算法都归结到了和非延迟调度相同的处理流程上,两者非常相似:
175 | 
176 | 1. 我们把非正的延迟当成是对非延迟 `schedule()` 调用。
177 | 2. 如果 worker 已经取消订阅,我们直接返回。
178 | 3. 我们实现记录和取消记录逻辑。
179 | 4. 我们利用 `genericScheduler` 来延迟在 `eventLoop` 上执行任务。
180 | 5. 在 `eventLoop` 上的执行逻辑和 `schedule()` 一样:把任务包装到 Runnable 中,并且在执行时检查是否已经取消,将 Runnable 提交到事件循环上,并且实现取消逻辑。
181 | 
182 | 最后,让我们实验一下:
183 | 
184 | ~~~ java
185 | Scheduler s = new GuiScheduler(new EDTEventLoop());
186 |  
187 | Observable source = Observable.just(1)
188 |         .delay(500, TimeUnit.MILLISECONDS, s)
189 |         .doOnNext(v -> {
190 |             try {
191 |                 Thread.sleep(1000);
192 |             } catch (InterruptedException ex) {
193 |                 ex.printStackTrace();
194 |             }
195 |             System.out.println(Thread.currentThread());
196 |         });
197 |  
198 | TestSubscriber ts1 = new TestSubscriber<>();
199 | TestSubscriber ts2 = new TestSubscriber<>();
200 | TestSubscriber ts3 = new TestSubscriber<>();
201 |  
202 | source.subscribe(ts1);
203 | source.subscribe(ts2);
204 | source.subscribe(ts3);
205 |  
206 | ts1.awaitTerminalEvent();
207 | ts1.assertNoErrors();
208 | ts1.assertValue(1);
209 |  
210 | ts2.awaitTerminalEvent();
211 | ts2.assertNoErrors();
212 | ts2.assertValue(1);
213 |  
214 | ts3.awaitTerminalEvent();
215 | ts3.assertNoErrors();
216 | ts3.assertValue(1);
217 | ~~~
218 | 
219 | 输出应该是这样的:
220 | 
221 | ~~~ java
222 | Thread[AWT-EventQueue-0,6,main]
223 | Thread[AWT-EventQueue-0,6,main]
224 | Thread[AWT-EventQueue-0,6,main]
225 | ~~~
226 | 
227 | ## 总结
228 | 
229 | 在关于 `Scheduler` 的最后一篇文章(本文)中,我讲解了如何把事件循环系统的 API 包装为 RxJava 的 Scheduler API。
230 | 
231 | 通常来说,处理 `Scheduler` API 或者我们想要包装的 API 时会遇到一些微妙的问题。这些“单独”的问题很难在本文中进行抽象化,所以如果你有什么有趣或者困难的 API 需要包装,你可以在 [StackOverflow 的 rx-java 话题](http://stackoverflow.com/questions/tagged/rx-java){:target="_blank"}中提问,或者我们的 [Google 群组](https://groups.google.com/forum/#!forum/rxjava){:target="_blank"}中提问,或者直接在本文下评论,以及在 [twitter 上 @akarnokd](https://twitter.com/akarnokd){:target="_blank"} 联系我。
232 | 
233 | [Reactive-streams](https://github.com/reactive-streams/reactive-streams-io){:target="_blank"} 最近似乎变得越来越知名,但由于它并没有提供太多超出已有的互操作 API 的内容(例如没有 `flatMap`),很多人开始编写一次性的 `Publisher`,并且对它的 `Subscription` 模型的行为产生了疑问。鉴于 RxJava 2.0 将原生支持 reactive-streams API,我们对 `Producer` 的知识将在处理 reactive-streams 的 `Subscription` 时变得很有用。在接下来的系列博客中,我将讲解 reactive-streams API,并且展示如何把 RxJava 的 `Producer` 转换为 `Subscription`。
234 | 


--------------------------------------------------------------------------------
/_posts/2016-07-15-operator-concurrency-primitives-subscription-containers-1.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: Operator 并发原语: subscription-containers(一),标准容器类与 TwoSubscribers
  4 | tags:
  5 |     - Operator
  6 |     - Subscription
  7 | ---
  8 | 
  9 | 原文 [Operator concurrency primitives: subscription-containers (part 1)](http://akarnokd.blogspot.com/2015/05/operator-concurrency-primitives_19.html){:target="_blank"}
 10 | 
 11 | ## 介绍
 12 | 
 13 | 编写复杂的操作符通常涉及到 subscription 管理以及一系列的 subscription 容器。
 14 | 
 15 | 有一点值得重申:subscription 容器都有一个终结状态,`unsubscribe()` 函数被调用之后它们就处于终结状态了,此后增加或者替换 subscription 时,都会取消订阅这个新的 subscription。这一点看起来可能有些奇怪,但接下来描述的一些场景将会解释这一设计决策背后的原因。
 16 | 
 17 | 假设你的操作符用 `Scheduler.Worker` 实例调度了一个延迟任务,此时并发的线程取消订阅了整个(_Observable_)链条,而 `schedule()` 函数还没有返回 `Subscription`,所以你也就无从取消这个延迟任务。如果订阅容器没有终结状态属性,那么把(_`schedule()` 函数返回的_)`Subscription` 加入到容器之后,这个容器就永远也不会取消订阅这个 subscription 了(_因为使用方已经调用过 `unsubscribe()` 了,他不会再调用了_),这时就发生了内存泄漏。有了终结状态属性之后,如果我们尝试把(_`schedule()` 函数返回的_)`Subscription` 加入到容器中,容器会立即取消订阅这个 subscription,相关资源也就立即释放了。这一点在安卓开发领域广为人知,因为在 app 暂停之后,我们会希望取消(_Observable_)链条中的后台任务。
 18 | 
 19 | 还有其他一些我们早已知晓的特性:
 20 | 
 21 | + 所有对容器的操作,从并发取消订阅的角度来说,都必须是线程安全和原子性的;
 22 | + `unsubscribe()` 必须是幂等的(多次调用没有任何作用);
 23 | + 我们应该避免在持有锁时调用 `unsubscribe()`(尤其是调用方有权持有锁时),以避免任何潜在的死锁情况。
 24 | 
 25 | 在本文中,我将简要介绍这些容器类,它们在操作符中的使用,以及值得注意的属性。但本文更深刻的价值是介绍这些容器类背后的概念和思想,以帮助大家开发自定义的容器类,以支持在 `Subject` 以及诸如 `publish()` 的转换操作符中的复杂生命周期。
 26 | 
 27 | ## 标准 subscription 容器类
 28 | 
 29 | ### CompositeSubscription
 30 | 
 31 | 用得最多的容器就是 `CompositeSubscription` 了。任何需要和资源(`Subscriber` 或者 `Future` 包装类型)打交道的操作符,通常都会选择这个容器。当前的实现版本使用了 `synchronized` 代码块来保证线程安全,并把 subscription 保存在 `HashSet` 中。
 32 | 
 33 | (_注:之所以不用 copy-on-write 的非阻塞方式,是由于 copy-on-write 会产生很多垃圾对象(很快被垃圾回收的对象),而在 Netflix 对 RxJava 的一些特定使用场景让他们比较担心 GC 的性能。_)
 34 | 
 35 | 一个值得注意的特性是,当我们调用 `remove()` 从中移除一个 subscription 时,如果这个 subscription 确实在容器中,那这个 subscription 会被取消订阅。这一点有可能并非我们想要的效果,例如我们有一个 `Future` 的包装类型放入了容器中,我们希望在它执行完毕之后从容器中移除,此时我们可能并不希望它被取消订阅。
 36 | 
 37 | 有些操作符它们需要处理的资源(_的添加和移除_)具有不确定性,对这些操作符来说 `CompositeSubscription` 就很合适,例如 `merge()`,此外其内部的 `HashSet` O(1) 的操作复杂度在性能上也很有优势。
 38 | 
 39 | ### SerialSubscription
 40 | 
 41 | `SerialSubscription` 同时只能包含一个 subscription,在设置新的 subscription 时它会取消订阅老的 subscription。在其内部它使用了 copy-on-write 方式加上一个状态变量来实现。
 42 | 
 43 | 这种容器主要用于需要每次关注一个资源的情形,例如 `concatMap()` 中当前的 `Subscriber`。
 44 | 
 45 | ### MultipleAssignmentSubscription
 46 | 
 47 | `MultipleAssignmentSubscription` 是 `SerialSubscription` 的一个变体(尽管类继承关系不一样),它也用于一次只包含一个 subscription,但是设置新的 subscription 时不会取消订阅老的 subscription。
 48 | 
 49 | 由于这一特性,它的使用场景更少,也需要更加谨慎。只有当它包含的那个 subscription 可以轻易“丢失”时,也就是说调用一次 `unsubscribe()` 是多余的,我们才可以使用它。这一容器被应用于通过 `Scheduler.Worker` 来实现定期调度的算法,在算法中我们会发出独立的延迟调度。`schedulePeriodically()` 返回的类型就是 `MultipleAssignmentSubscription`,它会包含当前的延迟调度。由于我们是在每个延迟调度结束的时候替换新的 subscription,所以我们没必要甚至不想要取消订阅老的 subscription。
 50 | 
 51 | ### SubscriptionList
 52 | 
 53 | `SubscriptionList` 是一个内部的实现,它和 `CompositeSubscription` 类似,只不过使用 `LinkedList` 保存 subscription 而不是 `HashSet`,这样可以让只有添加没有移除的场景更加高效。通常来说开发者不应该依赖内部实现,但如果你想要给 RxJava 提交 PR,我希望在合适的情况下你能使用这个容器类(而且它此时依然可以使用)。
 54 | 
 55 | 最近,这个容器类加上了 `remove()` 方法,以支持对默认 `computation()` 调度器的优化,加速对无延迟任务的添加以及移除,因为通常来说这些任务会按照同样的顺序执行与移除。所以我们每次移除的都是第一个元素,这比从 `HashMap` 移除元素效率要高(根据我们的 benchmark)。
 56 | 
 57 | 这一容器类出现在多处,最多的就是在 `Subscriber` 类中,但内部的 `ScheduledAction` 同样也使用了它。
 58 | 
 59 | ### RefCountSubscription
 60 | 
 61 | 在我看来 `RefCountSubscription` 是 Rx.NET 中一系列资源容器的一个遗民。它只会持有一个不能修改的 `Subscription`,并且“派发”从中派生出来的 subscription,而且当所有的派生 subscription 被取消订阅之后,它持有的 subscription 才会被取消订阅。我们在 RxJava 中很长时间都没有使用它,而在 Rx.NET 中,`RefCountDisposable` 工作得要比其他资源管理的 `IDisposables` 要好得多。
 62 | 
 63 | (_注:`BooleanSubscription` 不是一个容器类型,因为它没有持有其他 `Subscription` 的引用,它是用来容纳那些将要在取消订阅时被执行的任务的。_)
 64 | 
 65 | ## 实现阻塞的容器类型
 66 | 
 67 | 假设我们需要这样一个容器类型,它只能容纳两个 subscription,而且可以随时被替换(替换时老的 subscription 会被取消订阅)。让我们称之为 `TwoSubscribers`,它的结构如下:
 68 | 
 69 | ~~~ java
 70 | public final class TwoSubscribers 
 71 | implements Subscription {
 72 |     private volatile boolean isUnsubscribed;          // (1)
 73 |      
 74 |     Subscription s1;                                  // (2)
 75 |     Subscription s2;
 76 |      
 77 |     @Override
 78 |     public boolean isUnsubscribed() {
 79 |         return isUnsubscribed;                        // (3)
 80 |     }
 81 |      
 82 |     public void set(boolean first, Subscription s) {
 83 |         // implement
 84 |     }
 85 |      
 86 |     @Override
 87 |     public void unsubscribe() {
 88 |         // implement
 89 |     }
 90 | }
 91 | ~~~
 92 | 
 93 | 现在它看起来并不复杂。我们用 `volatile boolean` 来记录当前是否已经被取消订阅,以避免(1)(3)处不必要的 `synchronized` 代码块。这个类需要容纳两个 subscription,由于我们依赖外部的同步控制,所以我们直接使用了简单的成员变量(2)。
 94 | 
 95 | `set()` 函数接收一个 `boolean` 值来表明是哪个 subscription 需要被替换。它的实现如下:
 96 | 
 97 | ~~~ java
 98 | // ...
 99 | public void set(boolean first, Subscription s) {
100 |     if (!isUnsubscribed) {                       // (1)
101 |         synchronized (this) {
102 |             if (!isUnsubscribed) {               // (2)
103 |                 Subscription temp;               // (3)
104 |                 if (first) {
105 |                     temp = s1;
106 |                     s1 = s;
107 |                 } else {
108 |                     temp = s2;
109 |                     s2 = s;
110 |                 }
111 |                 s = temp;                        // (4)
112 |             }
113 |         }
114 |     }
115 |     if (s != null) {                             // (5)
116 |         s.unsubscribe();
117 |     }
118 | }
119 | // ...
120 | ~~~
121 | 
122 | 不像一直以来复杂的 `Producer`,这里代码看起来很简单:
123 | 
124 | 1. 我们提前检查是否已经被取消订阅,如果是那就可以跳过 `synchronized` 代码块,直接取消订阅参数即可。
125 | 2. 否则我们二次确认是否依然没有被取消订阅,如果没有被取消订阅,那我们就开始替换的逻辑。
126 | 3. 由于老的 subscription 需要被取消订阅,所以我们把它和参数进行交换。
127 | 4. 我们复用传入参数,让我们后面直接取消订阅 `s` 即可。
128 | 5. 我们可能从(1)(2)处执行到此,也可能在进行了替换之后执行到此,由于后者可能为 `null`,所以如果 `s` 不为 `null`,我们就取消订阅 `s`。
129 | 
130 | 现在就剩下 `unsubscribe()` 了:
131 | 
132 | ~~~ java
133 | // ...
134 | @Override
135 | public void unsubscribe() {
136 |     if (!isUnsubscribed) {                  // (1)
137 |         Subscription one;                   // (2)
138 |         Subscription two;
139 |         synchronized (this) {
140 |             if (isUnsubscribed) {           // (3)
141 |                 return;
142 |             }
143 |                 
144 |             isUnsubscribed = true;          // (4)
145 |                 
146 |             one = s1;                       // (5)
147 |             two = s2;
148 |                 
149 |             s1 = null;
150 |             s2 = null;
151 |         }
152 |             
153 |         List errors = null;      // (6)
154 |         try {
155 |             if (one != null) {
156 |                 one.unsubscribe();          // (7)
157 |             }
158 |         } catch (Throwable t) {
159 |             errors = new ArrayList<>();     // (8)
160 |             errors.add(t);
161 |         }
162 |         try {
163 |             if (two != null) {
164 |                 two.unsubscribe();
165 |             }
166 |         } catch (Throwable t) {
167 |             if (errors == null) {
168 |                 errors = new ArrayList<>();
169 |             }
170 |             errors.add(t);
171 |         }
172 | 
173 |         Exceptions.throwIfAny(errors);      // (9)
174 |     }
175 | }
176 | ~~~
177 | 
178 | 不幸的是,这个方法看起来没那么优雅,不过我们不得不这样:
179 | 
180 | 1. 我们检查容器是否已经被取消订阅了,如果是那我们就不用做任何事了。
181 | 2. 如果容器还没有被取消订阅,那我们就需要把当前引用的 subscription 摘出来,因为我们**不能在持有锁的时候取消订阅**(_译者注:还记得文章开头说的几点注意事项吗?_)。
182 | 3. 在 `synchronized` 代码块中,我们二次确认是否依然没有被取消订阅。
183 | 4. 我们尽早设置 `isUnsubscribed` 为 `true`,因为这样一来并发的 `set()` 调用就能尽早看到 `isUnsubscribed` 的新值,从而就不必等待进入 `synchronized` 代码块了。
184 | 5. 我们取出老的 subscription,并把成员置为 `null`,避免滞留它们的引用。
185 | 6. 谨防 `unsubscribe()` 函数中可能抛出的异常是一个很好的实践,而由于我们有两个 subscription,所以我们需要(_两个 try-catch 来_)保证它们都能被取消订阅并且异常也能被抛出。
186 | 7. 如果老的 subscription 不为 `null`,我们就将其取消订阅。
187 | 8. 如果发生异常,我们就创建一个异常列表,并把异常加入进去。
188 | 9. 在对两个 subscription 都执行了相同的逻辑之后,我们就利用一个辅助方法来抛出存在的异常,如果两个 subscription 的取消订阅过程都抛出了异常,那 `throwIfAny` 将会抛出一个包含两个异常的 `CompositeException`。
189 | 
190 | ## 总结
191 | 
192 | 本文相对来说比较简短,我在本文中回顾了已经存在的几种标准容器类型,包括它们内在的要求,属性,以及它们的使用要点。同时我也演示了如何通过阻塞同步操作实现一个和标准容器类型具有一致性的容器类型。
193 | 
194 | 在下一篇中,我将演示如何实现一个非阻塞版本的 `TwoSubscriptions`,并且将其扩展为可以容纳任意数量的 subscription 且保持非阻塞特性。
195 | 


--------------------------------------------------------------------------------
/_posts/2017-05-05-writing-custom-reactive-base-type.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: 如何编写自定义响应式基础类型
  4 | tags:
  5 | ---
  6 | 
  7 | 原文 [Writing a custom reactive base type](http://akarnokd.blogspot.com/2016/03/writing-custom-reactive-base-type.html){:target="_blank"}。
  8 | 
  9 | ## 介绍
 10 | 
 11 | 一直以来,大家都在问如何实现自己的响应式类型。尽管 RxJava 的 `Observable` 有大量方法,也允许通过 `lift()`、`extend()` 以及 `compose()` 进行扩展,大家仍会希望 Observable 拥有某个 `xyz()` 操作符,或者在某个调用链中不允许调用 `uvw()`。
 12 | 
 13 | 第一个情况,其实是希望在吃透整个 RxJava 项目之前就能增加自定义的操作符函数,而这个需求其实和 JVM 环境中的响应式编程同样古老。当我最初把 Rx.NET 移植到 Java 中时,我也面临了同样的问题,因为 .NET 早在 2010 年就支持了方法扩展。而 Java 并不支持方法扩展,而且这一特性的提议也在 Java 8 开发时期被否决,他们选择了默认方法这一特性,理由是扩展方法无法被重载。的确,扩展方法无法被重载,但它可以被另一个类的另一个方法替代。
 14 | 
 15 | 第二个情况,我们希望隐藏或者移除某些操作符,因为在我们自定义的 Observable 类型中,有些操作符没有意义。例如我们有一个 `ParallelObservable`,它把输入序列在内部分为并行处理的流水线,那么 `map()` 和 `filter()` 就是有意义的,而 `take()` 和 `skip()` 则没有意义。
 16 | 
 17 | ## 包装(Wrapping)
 18 | 
 19 | 上面的两种情况都可以通过包装 Observable 类来实现:
 20 | 
 21 | ~~~ java
 22 | public final class MyObservable {
 23 |     private Observable actual;
 24 |     public MyObservable(Observable actual) {
 25 |         this.actual = actual;
 26 |     }
 27 | }
 28 | ~~~
 29 | 
 30 | 然后我们就可以通过转发来增加我们的操作符了:
 31 | 
 32 | ~~~ java
 33 |     // ...
 34 |     public static  MyObservable create(Observable o) {
 35 |         return new MyObservable(o);
 36 |     }
 37 | 
 38 |     public static  MyObservable just(T value) {
 39 |         return create(Observable.just(value));
 40 |     }
 41 | 
 42 |     public final MyObservable goAsync() {
 43 |         return create(actual.subscribeOn(Schedulers.io())
 44 |             .observeOn(AndroidSchedulers.mainThread()));
 45 |     }
 46 | 
 47 |     public final  MyObservable map(Func1 mapper) {
 48 |         return create(actual.map(mapper));
 49 |     }
 50 | 
 51 |     public final void subscribe(Subscriber subscriber) {
 52 |         actual.subscribe(subscriber);
 53 |     }
 54 |     // ...
 55 | ~~~
 56 | 
 57 | 这样我们就实现了上述两个目标:去掉了不必要的操作符,也引入了新的操作符。
 58 | 
 59 | 如果我们看 RxJava 的源码,就会发现同样的模式,实际的对象都是 `OnSubscribe` 或者 `Publisher` 类型,而 `Observable` 则通过各种操作符极大地丰富了这些类型的功能。
 60 | 
 61 | ## 互操作(Interoperation)
 62 | 
 63 | `MyObservable` 看起来已经足够了,但最终我们会需要和普通的 `Observable` 或者是其他人实现的 `YourObservable` 互相操作。由于它们是不同的类型,我们需要一个公共的父类来使它们可以交互。最自然的想法当然是都实现一个 `toObservable()` 函数,返回内部的 `Observable`,但这导致我们必须调用这个额外的方法。相反,我们可以让每个自定义类型都继承自同一个基类,或者实现同一个接口,它们包含互相操作所需的最小方法集合。
 64 | 
 65 | 在 RxJava 1.x 中,显而易见的选项是 `Observable`,但它并不够好,因为它的方法都是 final 的,无法被被重写,而且这些方法都返回 `Observable` 而不是 `MyObservable`。不幸的是,RxJava 1.x 在这一点上无能为力,因为它要保证二进制兼容性。
 66 | 
 67 | 幸运的是,在 RxJava 2.x 中,最基本的类型并不是 `Observable`(`Flowable`),而是 `Publisher`。所有的 `Observable` 都是 `Publisher`,而很多操作符都是接受 `Publisher` 参数。这样的好处就是我们可以兼容 RxJava 2.x 之外的基于 `Publisher` 的类型。之所以可以做到这样,是因为操作符只需要它的上游有一个 `subscribe(Subscriber s)` 函数即可。
 68 | 
 69 | 因此,如果我们的目标是 2.x,那么 `MyObservable` 就应该实现 `Publisher` 接口,这样它就立即可以和其他正经的响应式编程库兼容了:
 70 | 
 71 | ~~~ java
 72 | public class MyObservable implements Publisher {
 73 |     // ...
 74 |     @Override
 75 |     public void subscribe(Subscriber subscriber) {
 76 |         actual.subscribe(subscriber);
 77 |     }
 78 |     // ...
 79 | }
 80 | ~~~
 81 | 
 82 | ## 扩展(Extension)
 83 | 
 84 | 有了这个 `MyObservable` 之后,我们可能需要不同的响应式类型以应对不同的使用场景,但复制所有的操作符就太繁琐了。第一想法当然是把 `MyObservable` 作为其他响应式类型的基类,但这同样会遇到 `Observable` -> `MyObservable` 的类型:操作符返回的类型不一样。
 85 | 
 86 | 我相信 Java 8 的 Streams API 也遇到了同样的问题,如果我们看看签名:`Stream extends BaseStream>` 和 `BaseStream>`,就会发现非常别扭,父类的类型参数居然需要子类型。这样做是为了能在方法签名中捕获子类型,这样如果我们创建了一个 `MyStream` 类型,那所有的方法的返回类型都将是 `MyStream`。
 87 | 
 88 | 我们也可以用类似的方式定义 `MyObservable` 类型:
 89 | 
 90 | ~~~ java
 91 | public class MyObservable> implements Publisher {
 92 |     
 93 |     final Publisher actual;
 94 |     
 95 |     public MyObservable(Publisher actual) {
 96 |         this.actual = actual;
 97 |     }
 98 |     
 99 |     @SuppressWarnings("unchecked")
100 |     public > U wrap(Publisher my) {
101 |         return (U)new MyObservable(my);
102 |     }
103 |     
104 |     public final > U map(Function mapper) {
105 |         return wrap(Flowable.fromPublisher(actual).map(mapper));
106 |     }
107 |     
108 |     @Override
109 |     public void subscribe(Subscriber s) {
110 |         actual.subscribe(s);
111 |     }
112 | }
113 | ~~~
114 | 
115 | 一大堆泛型的东西,我们编写了一个 `wrap()` 函数,用于把某种 `Publisher` 类型包装为 `MyObservable` 类型,并且在 `map()` 函数中调用了 `wrap()` 以确保正确的结果类型。`MyObservable` 的子类则要重写 wrap 函数以提供它们自己的类型:
116 | 
117 | ~~~ java
118 | public class TheirObservable extends MyObservable> {
119 |     public TheirObservable(Publisher actual) {
120 |         super(actual);
121 |     }
122 | 
123 |     @SuppressWarnings("unchecked")
124 |     @Override
125 |     public > U wrap(Publisher my) {
126 |         return (U) new TheirObservable(my);
127 |     }
128 | }
129 | ~~~
130 | 
131 | 让我们试一下:
132 | 
133 | ~~~ java
134 | public static void main(String[] args) {
135 |     TheirObservable their = new TheirObservable<>(Flowable.just(1));
136 |     
137 |     TheirObservable out = their.map(v -> v.toString());
138 | 
139 |     Flowable.fromPublisher(out).subscribe(System.out::println);
140 | }
141 | ~~~
142 | 
143 | 结果符合预期,没有编译错误,也能打印出 1。
144 | 
145 | 现在让我们为 `TheirObservable` 增加一个 `take()` 操作符:
146 | 
147 | ~~~ java
148 |     @SuppressWarnings({ "rawtypes", "unchecked" })
149 |     public > U take(int n) {
150 |         Flowable p = Flowable.fromPublisher(actual);
151 |         Flowable u = p.take(n);
152 |         return (U)(TheirObservable)wrap(u);
153 |     }
154 | ~~~
155 | 
156 | _译者注:原作者这里贴了 filter 的代码,我按照自己的理解写了 take 的代码。_
157 | 
158 | 函数签名变得越来越复杂了,类型系统正在反击!我们需要使用裸类型和强转来使得结果看起来像是目标类型。此外,如果我们编写 `their.map(v -> v.toString()).take(1);` 这样的代码,编译器会提示找不到 `take()` 函数。因为 `map()` 只有在我们把返回值赋值给 `MyObservable` 类型时,它才会返回 `MyObservable` 类型。为了让类型推导正确工作,我们不得不把链式调用拆分为单独的调用:
159 | 
160 | ~~~ java
161 |     TheirObservable their2 = new TheirObservable<>(Flowable.just(1));
162 |     TheirObservable step1 = their2.map(v -> v.toString());
163 |     TheirObservable step2 = step1.take(1);
164 |     Flowable.fromPublisher(step2).subscribe(System.out::println);
165 | ~~~
166 | 
167 | 接下来让我们把 `TheirObservable` 扩展为 `AllObservable`,然后增加 `filter()` 方法:
168 | 
169 | ~~~ java
170 | public static class AllObservable extends TheirObservable {
171 |     public AllObservable(Publisher actual) {
172 |         super(actual);
173 |     }
174 | 
175 |     @Override
176 |     > U wrap(Publisher my) {
177 |         return (U)new AllObservable(my);
178 |     }
179 |     
180 |     @SuppressWarnings({ "rawtypes", "unchecked" })
181 |     public > U filter(Predicate predicate) {
182 |         Flowable p = Flowable.fromPublisher(actual);
183 |         Flowable u = p.filter(predicate);
184 |         return (U)(AllObservable)wrap(u);
185 |     }
186 | }
187 | ~~~
188 | 
189 | 然后使用它:
190 | 
191 | ~~~ java
192 |     AllObservable all = new AllObservable<>(Flowable.just(1));
193 |     
194 |     AllObservable step1 = all.map(v -> v.toString());
195 | 
196 |     AllObservable step2 = step1.take(1);
197 |     
198 |     AllObservable step3 = step2.filter(v -> true);
199 | 
200 |     Flowable.fromPublisher(step3).subscribe(System.out::println);
201 | ~~~
202 | 
203 | 不幸的是,上面的代码无法编译,因为 `map()` 不是返回的 `AllObservable`,也就是说 `AllObservable` 不是 `MyObservable>`。把 `step1` 改成 `TheirObservable` 可以解决编译的问题。然而,如果我们想要交换 `filter()` 和 `take()` 的顺序,`step1` 就不再是 `AllObservable` 了,而 `filter()` 也就无法调用了。
204 | 
205 | ## 总结
206 | 
207 | 我们能解决 `AllObservable` 的问题吗?我不知道,就我对 Java 类型系统和类型接口的理解,现在只能做到这个程度了。
208 | 
209 | RxJava 2.x 会有这样的结构吗?如果由我来定,那肯定就是不会了。为了支持这样的结构,我们每次都需要包装,而我一直想摆脱所有的 `lift()` 和 `create()`,而且这样做也会让类型和函数的签名更加复杂。
210 | 
211 | 因此,如果有人希望这样做,上面的例子就展示了如何在不修改 RxJava 的情况下进行包装,并且可以自己控制暴露哪些 API。这也算是“组合优于继承”的一个很好的例子。
212 | 


--------------------------------------------------------------------------------
/_posts/2016-07-28-operator-concurrency-primitives-subscription-containers-2.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: Operator 并发原语: subscription-containers(二),无锁化 TwoSubscribers
  4 | tags:
  5 |     - Operator
  6 |     - Subscription
  7 | ---
  8 | 
  9 | 原文 [Operator concurrency primitives: subscription-containers (part 2)](http://akarnokd.blogspot.com/2015/05/operator-concurrency-primitives_22.html){:target="_blank"}
 10 | 
 11 | ## 介绍
 12 | 
 13 | 在本文中,我将实现[前文中](/AdvancedRxJava/2016/07/15/operator-concurrency-primitives-subscription-containers-1/index.html){:target="_blank"}介绍的 `TwoSubscribers` 的两种无锁化(_非阻塞_)版本。尽管它们在功能上完全一致,但是在实现的过程中将表现出在处理订阅状态检查和取消订阅时两种不同的哲学。
 14 | 
 15 | ## 在状态类中使用 `boolean isUnsubscribed`
 16 | 
 17 | 要实现一个无锁化的线程安全数据结构,一种简单地办法就是在每次修改时进行所谓的 “copy-on-write” 操作,即利用一个不可变的状态变量加上一个 CAS 循环。在这里,我们利用一个组合类来包含两个不同的 `Subscription`:
 18 | 
 19 | ~~~ java
 20 | static final class State {
 21 |     final Subscription s1;
 22 |     final Subscription s2;
 23 |     final boolean isUnsubscribed;
 24 |     public State(Subscription s1, 
 25 |             Subscription s2, 
 26 |             boolean isUnsubscribed) {
 27 |         this.s1 = s1;
 28 |         this.s2 = s2;
 29 |         this.isUnsubscribed = isUnsubscribed;
 30 |          
 31 |     }
 32 | }
 33 | // ...
 34 | ~~~
 35 | 
 36 | 有了 `State` 类之后,我们需要改变状态时,会先创建一个新的 `State` 对象(_为其设置相应的值_),再利用一个 CAS 循环保证原子性。现在让我们来看看新的无锁化容器类结构:
 37 | 
 38 | ~~~ java
 39 | public final class TwoSubscribersLockFree1 
 40 | implements Subscription {
 41 |     static final class State { 
 42 |         // ...
 43 |     }
 44 |  
 45 |     static final State EMPTY = 
 46 |         new State(null, null, false);                  // (1)
 47 |  
 48 |     static final State UNSUBSCRIBED = 
 49 |         new State(null, null, true);                   // (2)
 50 |      
 51 |     final AtomicReference state = 
 52 |         new AtomicReference<>(EMPTY);                  // (3)
 53 |  
 54 |     public void set(boolean first, Subscription s) {
 55 |         // implement
 56 |     }
 57 |      
 58 |     @Override
 59 |     public void unsubscribe() {
 60 |         // implement
 61 |     }
 62 |  
 63 |     @Override
 64 |     public boolean isUnsubscribed() {
 65 |         // implement
 66 |     }
 67 | }
 68 | ~~~
 69 | 
 70 | 首先,任何情况下初始状态和已经取消订阅的状态都是不变的,所以我们把它们声明为 `static final` 常量,它们只有 `isUnsubscribed` 值不同(1)(2)。由于状态的切换需要具备原子性,所以我们使用 `AtomicReference` 来引用 `State` 成员(3),它被初始化为初始状态。
 71 | 
 72 | 在上述结构之上,`set()` 的实现如下:
 73 | 
 74 | ~~~ java
 75 | public void set(boolean first, Subscription s) {
 76 |     for (;;) {
 77 |         State current = state.get();                    // (1)
 78 |         if (current.isUnsubscribed) {                   // (2)
 79 |             s.unsubscribe();
 80 |             return;
 81 |         }
 82 |         State next;
 83 |         Subscription old;
 84 |         if (first) {
 85 |             next = new State(s, current.s2, false);     // (3)
 86 |             old = current.s1;                           // (4)
 87 |         } else {
 88 |             next = new State(current.s1, s, false);
 89 |             old = current.s2;
 90 |         }
 91 |         if (state.compareAndSet(current, next)) {       // (5)
 92 |             if (old != null) {
 93 |                 old.unsubscribe();                      // (6)
 94 |             }
 95 |             return;
 96 |         }
 97 |     }
 98 | }
 99 | ~~~
100 | 
101 | 1. 读取当前状态。
102 | 2. 如果当前状态已经被取消订阅,说明我们已经到了终结状态,所以我们直接取消订阅 `s` 之后返回。
103 | 3. 否则,我们就基于当前状态创建一个新的状态,替换相应的 subscription。
104 | 4. 由于被替换的 subscription 需要被取消订阅,所以我们用一个局部变量保存被替换的 subscription。
105 | 5. 我们通过 CAS 操作来切换新旧状态,如果失败,说明当前有并发线程成功修改了状态,所以我们继续循环进行尝试。
106 | 6. 如果 CAS 成功且被替换的 subscription 不为 `null`,我们就将其取消订阅。
107 | 
108 | `isUnsubscribed()` 的实现非常直观:
109 | 
110 | ~~~ java
111 | // ...
112 | @Override
113 | public boolean isUnsubscribed() {
114 |     return state.get().isUnsubscribed;
115 | }
116 | // ...
117 | ~~~
118 | 
119 | 最后我们看一下 `unsubscribe()` 的实现:
120 | 
121 | ~~~ java
122 |     @Override
123 |     public void unsubscribe() {
124 |         State current = state.get();                        // (1)
125 |         if (!current.isUnsubscribed) {                      // (2)
126 |             current = state.getAndSet(UNSUBSCRIBED);        // (3)
127 |             if (!current.isUnsubscribed) {                  // (4)
128 |                 List errors = null;              // (5)
129 |                  
130 |                 errors = unsubscribe(current.s1, errors);   // (6)
131 |                 errors = unsubscribe(current.s2, errors);
132 |                  
133 |                 Exceptions.throwIfAny(errors);              // (7)
134 |             }
135 |         }
136 |     }
137 |  
138 |     private List unsubscribe(Subscription s,     // (8)
139 |             List errors) {
140 |         if (s != null) {
141 |             try {
142 |                 s.unsubscribe();
143 |             } catch (Throwable e) {
144 |                 if (errors == null) {
145 |                     errors = new ArrayList<>();
146 |                 }
147 |                 errors.add(e);
148 |             }
149 |         }
150 |         return errors;
151 |     }
152 | }
153 | ~~~
154 | 
155 | 其中有几点值得一提:
156 | 
157 | 1. 我们读取当前的状态。
158 | 2. 如果当前状态就已经被取消订阅,那我们就什么也不用做了。
159 | 3. 否则我们用原子操作把状态设置为终结状态。
160 | 4. 如果更新之前的状态已经被取消订阅,那我们就可以返回了,否则,由于 `getAndSet` 是原子操作,只会有一个调用者成功进行了状态切换。这里无需进行 CAS 循环,得益于平台支持的 `getAndSet` 操作,我们已经实现了非阻塞。
161 | 5. 可能的异常被收集到 `errors` 中。
162 | 6. 我重构了取消订阅的代码,把取消订阅一个 subscription 的逻辑封装到函数中(_译者注:把函数调用的返回值赋值给 `errors` 非常重要!_)。
163 | 7. 如果取消订阅时发生了异常,我们就将其抛出。
164 | 8. 我们在一个辅助函数中取消订阅,并捕获异常。
165 | 
166 | ## 使用 `UNSUBSCRIBED` 状态的引用
167 | 
168 | 试想,终结状态除了 `isUnsubscribed` 变量的值之外,和其他状态依然是可以区分的(通过一个常量引用)。所以我们可以把 `State` 类中的 `isUnsubscribed` 变量去掉,在需要的地方直接对比当前状态和 `UNSUBSCRIBED` 状态的引用。
169 | 
170 | 所以我们的 `TwoSubscribersLockFree2` 中 `State` 变成了:
171 | 
172 | ~~~ java
173 | public final class TwoSubscribersLockFree2 implements Subscription {
174 |     static final class State {
175 |         final Subscription s1;
176 |         final Subscription s2;
177 |         public State(Subscription s1, 
178 |                 Subscription s2) {
179 |             this.s1 = s1;
180 |             this.s2 = s2;
181 |             
182 |         }
183 |     }
184 | ~~~
185 | 
186 | 由于我们去掉了 `isUnsubscribed` 变量,所以我们需要修改之前对它进行检查的代码:
187 | 
188 | ~~~ java
189 | // ...
190 | static final State EMPTY = new State(null, null);         // (1) 
191 | static final State UNSUBSCRIBED = new State(null, null);
192 | 
193 | final AtomicReference state
194 |     = new AtomicReference<>(EMPTY);
195 | 
196 | public void set(boolean first, Subscription s) {
197 |     for (;;) {
198 |         State current = state.get();
199 |         if (current == UNSUBSCRIBED) {                    // (2)
200 |             s.unsubscribe();
201 |             return;
202 |         }
203 |         State next;
204 |         Subscription old;
205 |         if (first) {
206 |             next = new State(s, current.s2);
207 |             old = current.s1;
208 |         } else {
209 |             next = new State(current.s1, s);
210 |             old = current.s2;
211 |         }
212 |         if (state.compareAndSet(current, next)) {
213 |             if (old != null) {
214 |                 old.unsubscribe();
215 |             }
216 |             return;
217 |         }
218 |     }
219 | }
220 | 
221 | @Override
222 | public boolean isUnsubscribed() {
223 |     return state.get() == UNSUBSCRIBED;                    // (3)
224 | }
225 | 
226 | @Override
227 | public void unsubscribe() {
228 |     State current = state.get();
229 |     if (current != UNSUBSCRIBED) {                         // (4)
230 |         current = state.getAndSet(UNSUBSCRIBED);
231 |         if (current != UNSUBSCRIBED) {                     // (5)
232 |             List errors = null;
233 |             
234 |             errors = unsubscribe(current.s1, errors);
235 |             errors = unsubscribe(current.s2, errors);
236 |             
237 |             Exceptions.throwIfAny(errors);
238 |         }
239 |     }
240 | }
241 | // ...
242 | ~~~
243 | 
244 | 初始状态和终结状态再也不需要一个 `boolean` 变量了(1),而所有的 `current.isUnsubscribed` 都被替换为了 `current == UNSUBSCRIBED`(2,3,4,5)。
245 | 
246 | 这两种方式我们应该选择哪一种呢?运行一下 Benchmark 然后自己决定吧!显然,第一种方式分配了更多的内存,但在有些平台上,检查 `boolean` 更快,而第二种方式占用更少的内存,但是引用的对比相对慢一些。
247 | 
248 | 通常来说,使用上面的实现会增加 GC 的压力,因为每次修改状态都会进行新状态的创建。我们可以针对每个 subscription 进行单独的 CAS 循环,这样就不需要额外的 `State` 类了,也就不需要额外的对象创建了,但一旦 subscription 数量变多,代码就会变得非常冗长。
249 | 
250 | ## 总结
251 | 
252 | 在本文中,我介绍了 `TwoSubscribers` 的两种无锁化版本,并对其工作原理进行了讲解。
253 | 
254 | 但通常我们很可能需要管理不止两个 subscription,所以下篇文章中我将展示一种基于数组的容器类,并解析其工作原理。
255 | 


--------------------------------------------------------------------------------
/_posts/2017-04-21-rxjava-design-retrospect.md:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: post
  3 | title: RxJava 设计回顾
  4 | tags:
  5 | ---
  6 | 
  7 | 原文 [RxJava design retrospect](http://akarnokd.blogspot.com/2016/03/rxjava-design-retrospect.html){:target="_blank"}。
  8 | 
  9 | ## 介绍
 10 | 
 11 | RxJava 已经发布三年多了,期间也经历了好几次重大的版本变化。在本文中,我将指出一些我个人认为设计和实现过程中的不足之处。
 12 | 
 13 | 但不要误会,这并不是说 RxJava 不好,或者我知道怎么做得“更好”。这对所有参与其中的人来说都是一个学习的过程,关键是,我们能否从这些问题中吸取教训,在下一个大版本中做得更好。
 14 | 
 15 | ## 同步取消订阅
 16 | 
 17 | 在早些时候,RxJava 仿照了 Rx.NET 的架构,Rx.NET 的两大核心接口是 `IObservable` 和 `IObserver`,它们源自 `IEnumerable` 和 `IEnumerator` 接口。(我自己的 Reactive4Java 库也是这样的设计)
 18 | 
 19 | 在 `IObservable` 中,有一个 `subscribe()` 方法,它会返回一个 `IDisposable`。返回的 `IDisposable` 对象让我们可以取消整个运行的链条。但这一机制有个关键的问题,下面我用一个最简单的响应式程序演示一下:
 20 | 
 21 | ~~~ java
 22 | interface IDisposable {
 23 |     void dispose();
 24 | }
 25 |  
 26 | interface IObserver {
 27 |    void onNext(T t);
 28 | }
 29 | interface IObservable {
 30 |     IDisposable subscribe(IObserver observer);
 31 | }  
 32 |  
 33 |  
 34 | IObservable source = o -> {
 35 |    for (int i = 0; i < Integer.MAX_VALUE; i++) {
 36 |        o.onNext(i);
 37 |    }
 38 |  
 39 |    return () -> { };
 40 | };
 41 |  
 42 | IDisposable d = o.subscribe(System.out::println);
 43 | d.dispose();
 44 | ~~~
 45 | 
 46 | 如果我们运行上面的代码,它会向控制台打印数字,尽管我们立即对 subscribe 返回的对象调用了 dispose 方法。问题出在哪儿?
 47 | 
 48 | 问题在于数据源只有在结束循环之后,才能返回 `IDisposable` 对象,但此时已经没有意义了。整个过程都是同步的,因此也就不可能取消了。
 49 | 
 50 | 尽管 Rx 能很好地处理异步,但在一个典型的流水线中,很多步骤都是同步的,都会受到同步取消这一要求的影响。由于 Rx.NET 比 RxJava 还要年长 3 年,这么明显的问题怎么可能仍存在于 Rx.NET 中?
 51 | 
 52 | 上面示例中的代码就是著名的 `range()` 操作符,而如果我们在 C# 中执行类似的代码,就会发现并不会打印数字,或者很快停止。秘密就在于 Rx.NET 的 range 操作符默认是在异步的调度器中执行的,所以循环体会在异步线程中执行,而我们可以立即返回一个可以取消的 `IDisposable`。因此,同步取消的问题就被绕开了,但我不知道 Rx.NET 是有意还是无意为之,天晓得。
 53 | 
 54 | 如果我们看一下 Rx.NET range 的源码,我们会发现更复杂的东西。它利用了递归调度来向 Observer 传递每一个数据。经过测量,在同一台及其上传递 1M 个数据,Rx.NET 的速度只能达到 1M op/s,而 RxJava 能到 250M op/s。
 55 | 
 56 | RxJava 在 range 操作符中并未使用任何调度器,因此同步取消的问题就暴露出来了,我们利用了 `Subscriber` 类来解决这个问题。我们可以检查 subscriber 是否仍希望接收数据。我们可以重写上面的例子,在循环中检查 subscriber 的状态,并按需提前退出:
 57 | 
 58 | ~~~ java
 59 | Observable source = Observable.create(s -> {
 60 |    for (int i = 0; i < Integer.MAX_VALUE && !s.isUnsubscribed(); i++) {
 61 |        o.onNext(i);
 62 |    }
 63 | });
 64 |  
 65 | Subscription d = o.subscribe(new Subscriber() {
 66 |     @Override
 67 |     public void onNext(Integer v) {
 68 |         System.out.println(v);
 69 |         unsubscribe();
 70 |     }
 71 |  
 72 |     // ...
 73 | });
 74 | ~~~
 75 | 
 76 | 上面的 Subscriber 表现和 `take(1)` 一样,在收到第一个事件之后就取消自己。`unsubscribe()` 方法里面会设置一个 `volatile boolean` 标记,这个标记将在 `isUnsubscribed()` 中返回,因此循环就可以退出了。注意,我们仍无法通过 `subscribe()` 返回的 `Subscription` 函数取消订阅,因为 lambda 表达式中的循环在终止条件满足之前都不会退出。
 77 | 
 78 | 看起来我们并没有很好地解决最初的问题,对吧?但我们已经可以在循环开始前,或者循环过程中取消订阅了,而第二种情况显然需要在另一个线程中实现:
 79 | 
 80 | ~~~ java
 81 | Subscriber s = new Subscriber() {
 82 |     @Override
 83 |     public void onNext(Integer v) {
 84 |         System.out.println();
 85 |     }
 86 |     // ...
 87 | }
 88 |  
 89 | Schedulers.computation().schedule(s::unsubscribe, 1, TimeUnit.SECONDS);
 90 |  
 91 | source.subscribe(s);
 92 | ~~~
 93 | 
 94 | 事实上,我们可以在实际订阅之前设置好取消订阅逻辑,这样即便在最复杂的操作符中,我们也能恰当地把取消订阅的操作传播出去了。
 95 | 
 96 | 此外,上面的结构中还有一层更深的暗示。在我们创建的 Observable 中,lambda 表达式并没有任何东西,但 `Observable.subscribe()` 调用仍会返回一个实际上和传入的 `Subscriber` 参数一样的 `Subscription`。(技术上来说,这个话题涉及到的内容比较多,可以看看 [Jake Wharton 对这个话题的一个精彩演讲](http://jakewharton.com/presentation/2015-11-05-oredev))
 97 | 
 98 | 更进一步:如果我们需要返回什么东西,那我们就不可能彻底地响应式。返回数据就意味着同步的行为,函数就需要产生结果,即便此时它并不能产生这一结果。这时我们就只能阻塞或者 sleep 到实际产生数据的代码执行完毕了。这一点我在[关于 OSGi Asynchronous Event Streams 的文章中](/AdvancedRxJava/2017/03/25/asynchronous-event-streams-vs-reactive/index.html)有讲述。
 99 | 
100 | ## Subscriber 的资源管理
101 | 
102 | `Subscriber` 类允许我们通过 `Subscription` 对象的形式把资源和它绑定起来。当 subscriber/操作符被取消(或者终止)时,这些资源也就被取消了。
103 | 
104 | 这对操作符的开发者当然方便,但也有其代价:内存分配。
105 | 
106 | 我们通过默认构造函数创建 Subscriber 对象时,其内部的 `SubscriptionList` 也会被创建,无论它是否会被关联资源。在上面的例子中,`range()` 操作符并不需要资源管理,因此 `SubscriptionList` 并没有任何作用。
107 | 
108 | 一方面,很多操作符并不需要资源管理,因此创建这个容器就是一种浪费,而另一方面,也有很多操作符需要资源管理,因此很需要这种便利。
109 | 
110 | 此外,如果我们回忆一下 `Subscriber` 的内容,会发现它还有另一个构造函数,它接收另一个 `Subscriber` 对象,这样我们就可以共享内部的 `SubscriptionList` 对象了。显然,这可以减少一些内存分配,但绝大多数操作符都不能共享内部的 `SubscriptionList` 对象,因为这就会取消订阅下游了(请见 [pitfall #2](http://akarnokd.blogspot.hu/2015/05/pitfalls-of-operator-implementations.html))。因此 `Subscriber` 的结构,相对于操作符编写者的便利之处来说,在性能的角度来说更是个负担。
111 | 
112 | 你可能会想,给操作符编写者方便的工具有什么不对的?我承认 RxJava 之外的操作符应该尽可能多地带来帮助,但我相信,内部的操作符应该从开始就使用性能更好(即便麻烦一些)的方式实现。
113 | 
114 | 我曾好几次尝试解决这个问题,但鉴于 RxJava 1.x 的架构,我很怀疑这个问题能否被解决。幸运的是,Reactive-Stream 的架构以及 RxJava 2.x,通过把资源管理交给了操作符,解决了这个问题,
115 | 
116 | ## Subscriber 的 request() 函数
117 | 
118 | 如果我们看看 `Subscriber` 的实现,我们会发现一个 `protected` 的 `request()` 函数。这让我们可以很方便地发出请求,并保证如果当前已经通过 `setProducer` 设置了 `Producer`,那就把请求转发给它,如果没有 `Producer`,那就积累请求直到 `Producer` 到来。基本上来说,这就是一个内联的 [producer-arbiter](/AdvancedRxJava/2016/07/02/operator-concurrency-primitives-7/index.html)。
119 | 
120 | 有人可能会觉得这个函数的实现为请求管理带来了很大的开销,但 JMH benchmark 确认了它的影响在 +/- 3% 之内,而误差都可能会导致这种规模的差异。
121 | 
122 | 真正的问题在于它的名字和 `Producer.request` 一样,这就使得我们在继承 `Subscriber` 时无法实现 `Producer` 接口了。
123 | 
124 | 这一问题带来的后果就是,如果主 `Subscriber` 需要做请求管理,就需要一个额外的 `Producer` 对象。
125 | 
126 | 这会导致短序列在订阅时会对 GC 造成较大的影响。另一个影响是这会增加调用栈深度,这可能会阻碍一些 JIT 优化。
127 | 
128 | 由于 `Subscriber.request()` 也是公开 API 的一部分,所以在 1.x 中我们没法把它重命名进而为 `Producer.request()` 腾出空间。
129 | 
130 | 同样,解决方案也在 2.x 中:在 Reactive-Stream 中,`Subscriber` 和 `Subscription` 都是接口,它们可以同时出现,此外,`request()` 函数的便利可以转移到一个方便的 `Subscriber` 实现中(例如 `AsyncSubscriber`),这样就不会影响操作符的内部实现了。(这也意味着我们不鼓励在操作符中利用方便的 `Subscriber`)
131 | 
132 | ## lift
133 | 
134 | 和 backpressure 一起,`Observable.lift()` 被很多人认为是对 RxJava 最好的一个补充。它让我们可以深入订阅的过程中,利用下游的 Subscriber,返回一个新的 Subscriber 给上游,在其中执行操作符的逻辑。
135 | 
136 | lift 非常流行,现在几乎所有的操作符都使用了它。
137 | 
138 | 不幸的是,有得必有失:内存分配。对绝大多数操作符来说,应用这一操作符会增加 3 个额外的对象分配。为了展示这一点,让我们展开 map 的实现:
139 | 
140 | ~~~ java
141 | public final  Observable map(Func1 func) {
142 |     OperatorMap op = new OperatorMap(func);
143 |     return new Observable(new OnSubscribe() {
144 |         @Override
145 |         public void call(Subscriber child) {
146 |             Subscriber parent = op.call(child);
147 |             Observable.this.unsafeSubscribe(parent);
148 |         }
149 |     });
150 | }
151 | ~~~
152 | 
153 | 我们每次都会创建了一个 Operator 对象,一个 Observable 对象,以及一个 OnSubscribe 对象。
154 | 
155 | 直接使用 map 时这可能不是什么大问题,但想象一下在一个百万次的循环中,每次都有 3 个额外的分配会有怎样的影响?在 flatMap 中如果我们要使用 map 就会出现这样的情况:
156 | 
157 | ~~~ java
158 | Observable.range(1, 1_000_000).flatMap(v -> 
159 |     Observable.just(v).observeOn(Schedulers.computation()).map(v -> v * v))
160 | .subscribe(...);
161 | ~~~
162 | 
163 | lift 操作符实际上就是一个 OnSubscribe 对象,它会捕获上游的 Observable,并对下游的 `Subscriber` 调用 `Operator.call`。显然我们可以直接用 OnSubscribe 实现操作符,并把上游的 Observable 作为参数,这样尽管对象的实际大小并没有多大变化,但内存分配和调用栈都会减少。
164 | 
165 | 当前 lift 的结构还有一个不利的影响:它让操作符融合从困难变成了不可能。因为它是一个异步的类,我们很难获得上游的 Observable 和 Operator;而且即便我们创建了一个命名类,这两个类对象都无法直接获得,要取得它们会带来更多的额外开销。
166 | 
167 | 幸运的是,这里提到的问题我们都可以在不影响公开 API 的前提下解决,只不过需要多写以及多审阅数千行的代码。
168 | 
169 | 不幸的是,我在去年九月实现 RxJava 2.0 预览版的时候并没有考虑过 lift 的开销,因此 2.x 也大量使用了 lift。
170 | 
171 | 不过,在这条道路的尽头还是有光明的:Reactor 2.5 并没有走上 lift 这条路,现在它的开销比 RxJava 小。
172 | 
173 | ## create
174 | 
175 | 最近我开始公开反对 `Observable.create()`,现在我认为我们应该为它改一个更可怕的名字,这样初学者就会避免使用它,进而寻找更合适的工厂方法来创建 Observable 了,以更好地处理 backpressure 和取消订阅。我们可以把它看做一个向听众展示如何进入响应式编程世界的工具,但它确实不应该在演讲中获得这么多关注。
176 | 
177 | 除了这些,create() 的问题还在于它会为每个 Observable 创建两个对象:Observable 对象本身,以及容纳订阅逻辑的 OnSubscribe 对象。
178 | 
179 | 通过 create() 创建 Observable 的做法源自“组合优于继承”的倡导。从普适的设计原则角度来讲,这种做法是没问题的,但我们在 Java 的世界里面需要意识到,组合就意味着内存分配:外部类对象,内部类对象,已经“内部的内部”类对象。
180 | 
181 | 为了避免这些内存分配,解决办法就是让 Observable 不把使用 OnSubscribe 对象作为默认选项(但可以把 create() 保留着,作为一个 lambda 工厂的形式),而且操作符(包括源头中间的操作符)应该直接继承自 Observable。所有的操作符函数都继续保留在 Observable 类中:
182 | 
183 | ~~~ java
184 | public final  Observable map(Func1 func) {
185 |     return new ObservableMap(this, func);
186 | }
187 | ~~~
188 | 
189 | 这样,没有了 lift,create 之后,map 只需要每次分配一个 Observable 对象即可。
190 | 
191 | 我相信这些改变并不会影响到公开的 API,因为 Observable 的函数是 static 或者 final 的,而操作符则都是 Observable 的子类。这一改变也有利于操作符融合,因为每个上游都可以直接区分开来了,它们的参数也可以直接暴露出来了。
192 | 
193 | 这里 Reactor 2.5 再次领先了 RxJava,它没有使用 create。它的操作符都是通过继承 [Flux](https://github.com/reactor/reactor-core/blob/master/src/main/java/reactor/core/publisher/Flux.java) 实现的。
194 | 
195 | ## 总结
196 | 
197 | 设计和实现 RxJava 的各个版本一直都是一个学习的过程,而且也会出现一些影响性能和复杂度的意外效果。
198 | 
199 | 你可能会想,为什么这些结构上的麻烦以及内存分配的开销,在当前的状态下运行着?两个原因:云计算和 Android/IoT。在云计算领域,事件都数以十亿计,任何性能的开销都会被急剧放大。你可能无法轻易地计算出上面 flatMap 的例子在笔记本上面的开销,但云计算是按秒,按 GB,GHz 计费的。而对 Android 和 IoT 来说,设备的资源限制以及越来越多的需求要求我们把内存占用、GC 以及电池消耗都考虑进来。
200 | 


--------------------------------------------------------------------------------
/_layouts/page.html:
--------------------------------------------------------------------------------
  1 | ---
  2 | layout: default
  3 | ---
  4 | 
  5 | 
  6 | 
7 |
8 |
9 |
10 |
11 |

{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}

12 | 13 | {{ page.description }} 14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 | {% if site.sidebar == false %} 24 | 25 | 26 |
27 | {{ content }} 28 |
29 | 30 | 63 | {% else %} 64 | 65 | 66 | 67 |
74 | {{ content }} 75 |
76 | 77 | 183 | {% endif %} 184 |
185 |
186 | -------------------------------------------------------------------------------- /_posts/2016-05-13-operator-concurrency-primitives-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Operator 并发原语:串行访问(serialized access)(二),queue-drain 4 | tags: 5 | - Operator 6 | --- 7 | 8 | 原文 [Operator concurrency primitives: serialized access (part 2)](http://akarnokd.blogspot.com/2015/05/operator-concurrency-primitives_11.html){:target="_blank"} 9 | 10 | ## 介绍 11 | 在[Operator 并发原语:串行访问(serialized access)(一)](/AdvancedRxJava/2016/05/06/operator-concurrency-primitives/index.html){:target="_blank"}中,我介绍了串行访问的需求,它是为了让 RxJava 的一些方法以及操作符可以串行执行。我详细介绍了**发射者循环(emitter-loop)**,并展示了如何利用它来实现串行访问的约定。我想再次强调一点,**在大部分的单线程使用场景下,这种实现方式性能表现非常突出,因为 Java (JVM) 的 JIT 编译器会在检测到只有单线程使用时使用偏向锁和锁省略技术**。然而如果异步执行占了主要部分,或者发射操作在另一个线程执行,由于其阻塞(blocking)的原理,发射者循环就会出现性能瓶颈。 12 | 13 | 在本文中,我将介绍另一种非阻塞的串行实现方式,我称之为**队列漏(queue-drain)**。 14 | 15 | ## 队列漏(queue-drain) 16 | 相比于发射者循环,队列漏的代码相对简短: 17 | 18 | ~~~ java 19 | class BasicQueueDrain { 20 | final AtomicInteger wip = new AtomicInteger(); // (1) 21 | public void drain() { 22 | // work preparation 23 | if (wip.getAndIncrement() == 0) { // (2) 24 | do { 25 | // work draining 26 | } while (wip.decrementAndGet() != 0); // (3) 27 | } 28 | } 29 | } 30 | ~~~ 31 | 32 | 它的实现原理是这样的: 33 | 34 | 1. 我们需要有一个可以进行原子自增操作的数字变量,我通常称之为 `wip` (work-in-progress 的缩写)。它用来记录需要被执行的任务数量,只要 Java runtime 底层实现具有支持(2)和(3)操作的原语,那我们实现的队列漏甚至是完全没有等待(阻塞)的(_译者注:CPU 如果支持 compare and set 指令,那自增操作就只需要一条指令_)。在 RxJava 中我们使用了 `AtomicInteger`,因为我们认为在通常的场景下,溢出是不可能发生的。当然,如果需要的话,它完全可以替换为 `AtomicLong`。 35 | 2. 我们利用原子操作,获取 `wip` 当前的值,并对它进行加一操作。只有把它从零增加到一的线程才能进入后面的循环,而其他所有的线程都将在加一操作之后返回。 36 | 3. 每当一件任务被取出(漏出)并处理完毕,我们就对 `wip` 进行减一操作。如果减到了零,那我们就退出循环。由于我们只有一个线程在进行减一操作,我们就能保证不会发生信号丢失。由于(2)和(3)操作都是原子性的,所以如果执行(3)时 `wip == 1`,而(2)先执行完毕,那(3)得到的 `wip` 值仍然为一,那就会再进行一次循环;而如果(3)先执行完毕,那(2)就是把 `wip` 从零增加到一的线程,那它就会进入后面的循环,而由于(3)先执行完毕,那它也就会退出循环了,两者没有任何干扰。 37 | 38 | 如果你还记得上篇中发射者循环的例子,我们就可以得出两种方式的数据结构之间有一些相似之处。 39 | 40 | 在发射者循环中,如果一个线程看到的是 `emitting == false` 而且把 `emitting` 置为了 `true`,那这个线程就获得了发射的权利,而在队列漏中,如果一个线程在原子自增操作中,是把 `wip` 从零增加到一,那这个线程就获得了漏的权利。在发射者循环中,如果一个线程没有获得发射的权利,那它会把 `missed` 置为 `true`,而在队列漏中,如果一个线程没有获得漏的权利,它就会继续增加 `wip`(增加为 2 或者更大)。在结束循环的检查中,发射者循环会检查是否 `missed` 已被置为 `true`,如果是就继续循环,在队列漏中,如果 `wip` 在减一之前大于一(减之后大于零),那就会继续循环。在发射者循环中,如果 `missed` 为 `false`,那就退出循环,在队列漏中,如果减一之后 `wip` 为零,就退出循环。 41 | 42 | 记住这些之后,我们就可以在上篇例子的基础上,利用队列漏方式实现一个非阻塞的 `ValueEmitterLoop` 版本了: 43 | 44 | ~~~ java 45 | class ValueQueueDrain { 46 | final Queue queue = new MpscLinkedQueue<>(); // (1) 47 | final AtomicInteger wip = new AtomicInteger(); 48 | Consumer consumer; // (2) 49 | 50 | public void drain(T value) { 51 | queue.offer(Objects.requireNonNull(value)); // (3) 52 | if (wip.getAndIncrement() == 0) { 53 | do { 54 | T v = queue.poll(); // (4) 55 | consumer.accept(v); // (5) 56 | } while (wip.decrementAndGet() != 0); // (6) 57 | } 58 | } 59 | } 60 | ~~~ 61 | 62 | 和发射者循环的版本比较类似,但由于没有了 `synchronized` 代码块,队列漏更加精简: 63 | 64 | 1. 我们继续使用 [JCTools](https://github.com/JCTools/JCTools){:target="_blank"} 的优化版本来保存待消费的数据。如果可能的最大待消费数据数量是确定的,那就可以使用 `MpscArrayQueue`,不过需要注意,它提供的 API 与 `offer` / `poll` 相比,[并不是非阻塞的](http://psy-lob-saw.blogspot.com/2015/04/porting-dvyukov-mpsc.html){:target="_blank"}。 65 | 2. 我们把队列里的数据一个一个漏出来给 `consumer`。 66 | 3. 首先,我们把不为 `null` 的数据加入到队列中。 67 | 4. 获得漏的权利的线程负责从队列中取出数据。由于 `wip` _大体上_ 记录了队列中的数据个数,`poll()` 不会返回 `null`(_`wip` 的值一定不小于 `queue` 的元素个数_)。 68 | 5. 我们把取出的数据传递给 `consumer`。 69 | 6. 如果 `wip` 减一之后不为零,我们就继续循环,否则退出循环。注意,确实有可能执行到(6)的时候,队列中还有元素,但这并不是问题,因为即便(3)的调用和增加 `wip` 之间存在延迟,当前的循环也可以退出,因为紧接着增加 `wip` 的时候,又会有新的线程进入循环。 70 | 71 | 如果我们在单线程场景下对 `ValueEmitterLoop` 和 `ValueQueueDrain` 进行性能测试,在启动完毕(JIT 生效)之后,后者的吞吐量会更低。 72 | 73 | 出现这样的现象,是因为队列漏方式存在无法避免的原子操作开销,即便在没有多线程进程的场景下,也会多消耗几个 CPU 周期,这是由现代多核 CPU 强制缓冲区写刷新(mandatory write-buffer flush)导致的。我们每次 `drain()` 一个数据的时候,有两次增减原子操作,以及 `MpscLinkedQueue` 使用的原子操作。而一旦 `ValueListEmitterLoop` 完成了锁优化之后,性能就会更好。 74 | 75 | _注意,我在上文提到的 benchmark 在 `consumer` 的回调中执行的代码非常简单,以此来测量串行执行的开销。不同的 `consumer` 以及不同的调用分布,都可能会影响测试结果。_ 76 | 77 | 我们可以优化一下我们的队列漏实现,让它在没有并发的时候,也能利用锁优化技术。 78 | 79 | 但是需要注意的是,这样的优化会让代码变得更长更复杂。所以我强烈建议**先对非优化版本进行测量,或者尝试 set-to-1 trick**(我将在后面讲解)。 80 | 81 | 下面我们看看第一个优化版本: 82 | 83 | ~~~ java 84 | class ValueQueueDrainFastpath { 85 | final Queue queue = new MpscLinkedQueue<>(); 86 | final AtomicInteger wip = new AtomicInteger(); 87 | Consumer consumer; 88 | 89 | public void drain(T value) { 90 | Objects.requireNonNull(value); 91 | if (wip.compareAndSet(0, 1)) { // (1) 92 | consumer.accept(value); // (2) 93 | if (wip.decrementAndGet() == 0) { // (3) 94 | return; 95 | } 96 | } else { 97 | queue.offer(value); // (4) 98 | if (wip.getAndIncrement() != 0) { // (5) 99 | return; 100 | } 101 | } 102 | do { 103 | T v = queue.poll(); // (6) 104 | consumer.accept(v); 105 | } while (wip.decrementAndGet() != 0); 106 | } 107 | } 108 | ~~~ 109 | 110 | 尽管 `ValueQueueDrainFastpath` 看上去使用了更多的原子操作,但实际上在单线程场景下,它不会调用到 `offer` 和 `poll`,因此性能更好一些。它的工作原理如下: 111 | 112 | 1. 在确保数据不为 `null` 之后,我们尝试对 `wip` 执行 CAS (compare-and-swap)操作,如果旧的值为 0,我们就将其置为 1(尝试把 `wip` 从 0 变为 1)。(注意,在 CAS 操作之前加上 `wip.get() == 0 &&` 判断也许可以提升性能,但这种提升是严重依赖于 CPU 类型以及调用分布的。我在不同的 CPU 类型下运行 benchmark,没有固定的结论。) 113 | 2. (_如果我们成功把 `wip` 从 0 变为 1_)我们直接把数据传递给 `consumer`,不经过 `queue` 中转。这一点就是本优化版本的性能提升点。 114 | 3. 调用 `consumer` 回调之后,我们需要对 `wip` 执行减一操作,而如果没有其他的线程调用 `drain()` 并且执行到(5),那我们就可以退出了。有人可能会觉得在这里我们也可以进行 CAS 操作(从 1 到 0),但如果 CAS 失败,我们仍然需要对 `wip` 进行减一操作,然后再进入到后面的循环中。 115 | 4. 如果(1)处的 CAS 失败,我们就进入到常规的处理流程(慢路径,slow-path),先把数据加入到队列中。 116 | 5. 把数据加入到队列之后,我们把 `wip` 加一,如果加一之前 `wip` 不为零,我们就返回了(我们返回的条件必须是非零,因为快路径和慢路径共用了同一个漏循环)。这个检查是非常必要的,因为有可能在当前线程执行到(5)之前,就有另一个线程进入了快路径并且执行完毕返回了,那此时 `wip` 依然是 0,所以当前线程依然是把 `wip` 从 0 变为 1 的线程,依然应该进入漏循环中。 117 | 6. 无论是快路径还是慢路径进入了漏循环,我们的执行就和普通队列漏循环一样了。当有线程在执行漏循环时,其他线程都讲在(1)处的 CAS 失败,进而进入到慢路径中。 118 | 119 | 需要注意的是,这个快路径优化版本是一个折衷方案(tradeoff):快路径优化(没有额外的队列原子操作)与慢路径恶化(多了一次失败的 CAS 操作)的一个折衷(或者加一个 `wip.get() == 0` 判断,能省去慢路径的 CAS 失败,但却取决于调用分布)。所以我要再次强调,**先测量,别假设**。 120 | 121 | 观察仔细的朋友可能已经注意到,`ValueQueueDrainFastpath` 和上篇中的 `ValueListEmitterLoop` 很相似:获得发射权利(漏权利)的线程,都先直接把数据传给 `consumer`,跳过队列操作,这是快路径;而如果此时有其他线程在循环中了(此时 `emitting` 为 `true` 或者 `wip` 大于 0),那就回退到慢路径中。 122 | 123 | 除了上述的快路径优化,我们还有一种优化方案可以减少队列漏中的原子操作。还记得我们在前文中对发射者循环和队列漏的对比吗?只要 `wip` 值大于 1,就意味着还有更多的数据,而队列本身可以告诉我们数据是否被处理完了。所以如果我们每次都把队列漏完,并且(_漏完之后_)在我们对 `wip` 进行减一操作之前如果又有新的数据被加入到队列中,我们就继续循环,那我们就可以在每次漏一个数据的时候减少一次原子操作: 124 | 125 | ~~~ java 126 | class ValueQueueDrainOptimized { 127 | final Queue queue = new MpscLinkedQueue<>(); 128 | final AtomicInteger wip = new AtomicInteger(); 129 | Consumer consumer; 130 | 131 | public void drain(T value) { 132 | queue.offer(Objects.requireNonNull(value)); 133 | if (wip.getAndIncrement() == 0) { 134 | do { 135 | wip.set(1); // (1) 136 | T v; 137 | while ((v = queue.poll()) != null) { // (2) 138 | consumer.accept(v); 139 | } 140 | } while (wip.decrementAndGet() != 0); // (3) 141 | } 142 | } 143 | } 144 | ~~~ 145 | 146 | `ValueQueueDrainOptimized` 和 `ValueQueueDrain` 看起来很相似,但工作原理略有不同: 147 | 148 | 1. 一旦我们进入漏循环,我们就把 `wip` 置为 1。由于我们会一次性把队列漏完,_如果执行(3)操作时,没有其他线程干扰_,那我们就无需再次循环了,因为队列已经漏空了,所以这里我们可以直接把 `wip` 置为 1。这和上篇的 `EmitterLoopSerializer` 中,我们在重新循环之前把 `missed` 置为 `false` 道理是一样的。 149 | 2. 接下来我们就通过一个内循环一次性漏空整个队列,而不是逐个进行。我们不需要判断 `queue.size()` 或者 `queue.isEmpty()`,因为如果 `poll` 返回了 `null`,就表明队列空了。如此我们就可以在每次漏一个数据的时候,节约一次原子操作。 150 | 3. 内循环退出后,我们对 `wip` 减一,如果减成零了,那我们就退出循环。在一种理想情况下,如果在执行(1)之前爆发性地往队列里添加了大量数据,我们会一次性把它们都处理完,并且由于我们把 `wip` 置为了 1,我们的漏循环只会执行一次。如果在执行(3)之前又有数据被加入到了队列中,这不会出现问题,因为原子操作会保证数据同步:所以要么(3)操作没有把 `wip` 减到零,进而重新循环,要么(3)把 `wip` 减到零并退出,进而让 `if (wip.getAndIncrement() == 0)` 通过并进入循环。 151 | 152 | 需要注意的是,这一优化版本同样有其限制。尽管我们把后续的所有数据一次性批量漏空,(2)处的缓存一致性(cache-coherence)保证,可能会让我们在某些情况下性能没有任何提升,所以这一版本的性能会取决于对 `drain()` 的调用分布。 153 | 154 | 通过上面的例子和解释,我们可能会觉得队列漏问题多多:它的优化空间以及性能表现都严重依赖于调用分布,因此通常情况下都会倾向于使用发射者循环。 155 | 156 | 但是,有一个几乎任何人都会使用的操作符就是使用的队列漏:`observeOn`。它目前的实现,使用了 `ValueQueueDrainOptimized` 中介绍的优化方案,因为它对队列(`SpscArrayQueue`)的 `offer` 和 `poll` 的操作都只在单一的线程中(两者是不同的线程)(当然,特定的 backpressure 场景除外,我将在后续的文章中进行更多的分析)。 157 | 158 | ## 总结 159 | 在本文中,我介绍了实现串行访问的第二种方案,我称之为**队列漏(queue-drain)**。我展示了一个基础的实现版本和两种优化的版本,优化版本的性能都依赖于对 `drain()` 的调用分布(以及 CPU 类型)。所以请**务必对你的实现版本进行测量**。 160 | 161 | 它使用的非阻塞无锁(甚至无等待)数据结构使得它在中高程度并发场景下性能更优,以及确定添加数据和消耗数据分别发生在不同的线程的场景下性能更优。 162 | 163 | 在接下来的文章中,我将介绍多种线程安全的 `Producer` 实现,它们其中一些就利用了**发射者循环(emitter-loop)**和**队列漏(queue-drain)**,我将在后续的文章中详细介绍。 164 | -------------------------------------------------------------------------------- /_posts/2016-08-26-schedulers-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 调度器 Scheduler(三):包装多线程 Executor 4 | tags: 5 | - Scheduler 6 | --- 7 | 8 | 原文 [Schedulers (part 3)](http://akarnokd.blogspot.com/2015/06/schedulers-part-3.html){:target="_blank"} 9 | 10 | ## 介绍 11 | 12 | 在本文中,我将讲讲如何把已有的多线程 `Executor` 包装为一个 scheduler,并且遵循 `Scheduler` 和 `Worker` 的规则。 13 | 14 | `Worker` 最重要的一个规则就是有序提交的非延迟任务要按序执行,但是 `Executor` 的线程是随机取走任务,而且是并发乱序执行的。 15 | 16 | 解决办法就是使用我们以前介绍过的“队列漏”,并且对调度的任务进行一个中继操作(_原文是 trampolining,意为“蹦床”,但没懂是什么意思,推测是“中继”吧_)。队列会保证任务提交的顺序得到了保存,而漏的逻辑则保证了任意时刻最多只会有一个任务执行,不会出现并发执行。 17 | 18 | ## `ExecutorScheduler` 19 | 20 | 首先,让我们看看类结构: 21 | 22 | ~~~ java 23 | public final class ExecutorScheduler extends Scheduler { 24 | final Executor exec; 25 | public ExecutorScheduler(Executor exec) { 26 | this.exec = exec; 27 | } 28 | @Override 29 | public Worker createWorker() { 30 | return new ExecutorWorker(); 31 | } 32 | 33 | final class ExecutorWorker extends Worker 34 | implements Runnable { // (1) 35 | // data fields here 36 | @Override 37 | public Subscription schedule( 38 | Action0 action) { 39 | // implement 40 | } 41 | @Override 42 | public Subscription schedule( 43 | Action0 action, long delayTime, 44 | TimeUnit unit) { 45 | // implement 46 | } 47 | @Override 48 | public void run() { 49 | // implement 50 | } 51 | @Override 52 | public boolean isUnsubscribed() { 53 | // implement 54 | } 55 | @Override 56 | public void unsubscribe() { 57 | // implement 58 | } 59 | } 60 | } 61 | ~~~ 62 | 63 | 所有的 worker 实例都会把任务转发到同一个底层的 `Executor` 上。需要注意的是,我们让 `ExecutorWorker` 实现了 `Runnable`,这样我们就可以在后面的漏逻辑中省去创建一个单独的 `Runnable` 实例。 64 | 65 | 漏逻辑需要一个队列、一个正在执行的标记、以及一个 subscription 容器类型,以便集中取消订阅: 66 | 67 | ~~~ java 68 | // ... 69 | final AtomicInteger wip = new AtomicInteger(); 70 | final Queue queue = new ConcurrentLinkedQueue<>(); 71 | final CompositeSubscription tracking = new CompositeSubscription(); 72 | // ... 73 | ~~~ 74 | 75 | 这里使用了 `ConcurrentLinkedQueue`,可能会让一些性能追求者(例如我)不满意,因为有可能我们只需要 [JCTools](https://github.com/JCTools/JCTools){:target="_blank"} 中的 `MpscLinkedQueue`,甚至是我实现的 [MpscLinkedArrayQueue](https://gist.github.com/akarnokd/0699387c4ed43f5d4386){:target="_blank"}。 76 | 77 | 这里需要权衡:我们是否愿意承担已取消任务仍被保留的问题。在基于 `ScheduledExecutorService` 的 `Scheduler` 中这并不是问题,因为它们会自动将已取消的任务从内部队列中移除(或者在 Java 6 的环境中定期清理)。但是这些删除操作在 JCTools 中都不能用,所以目前来看,如果不允许已取消任务仍被保留,最好的选择就是使用 `ConcurrentLinkedQueue`(当然,我们也可以实现一个特定的队列,以及特定的任务类,使得每个任务都知道其他任务的状态,所以取消订阅时可以定位到这个任务,然后用一个 CAS 操作将其替换为一个“墓碑”任务,_一个特殊的用来标记已取消的任务_)。但是要注意,这里移除操作的复杂度是 O(n)。 78 | 79 | 基于上面的数据成员,让我们先实现几个简单地函数: 80 | 81 | ~~~ java 82 | // ... 83 | @Override 84 | public boolean isUnsubscribed() { 85 | return tracking.isUnsubscribed(); 86 | } 87 | 88 | @Override 89 | public void unsubscribe() { 90 | queue.clear(); 91 | tracking.unsubscribe(); 92 | } 93 | } 94 | } 95 | ~~~ 96 | 97 | 注意,我们无权控制 `Executor` 的声明周期,所以不应该结束它,而且一旦我们结束了 `Executor`,其他的 worker 也就会停止工作了。最好的方式就是记录提交到这个 worker 的任务,并且将它们一起取消。 98 | 99 | 现在开始复杂部分了,首先让我们看看无延迟的 `schedule()`: 100 | 101 | ~~~ java 102 | @Override 103 | public Subscription schedule(Action0 action) { 104 | if (isUnsubscribed()) { 105 | return Subscriptions.unsubscribed(); 106 | } 107 | ScheduledAction sa = 108 | new ScheduledAction(action); 109 | tracking.add(sa); 110 | sa.add(Subscriptions.create( 111 | () -> tracking.remove(sa))); // (1) 112 | 113 | queue.offer(sa); // (2) 114 | 115 | sa.add(Subscriptions.create( 116 | () -> queue.remove(sa))); // (3) 117 | 118 | if (wip.getAndIncrement() == 0) { // (4) 119 | exec.execute(this); // (5) 120 | } 121 | 122 | return sa; 123 | } 124 | ~~~ 125 | 126 | 我们现在不能简单地将调用转发到延迟版本中了,它需要自己的逻辑: 127 | 128 | 1. 我们创建一个 `ScheduledAction`(在[第二部分中实现的](/AdvancedRxJava/2016/08/19/schedulers-2/index.html){:target="_blank"}),并且在其被取消订阅时将自己从 `tracking` 中移除。 129 | 2. 我们把任务加入到队列中,队列会保证任务按照提交顺序先进先出(FIFO)执行。 130 | 3. 我们在任务被取消时,将任务从队列中移除。注意这里的移除操作复杂度为 O(n),n 表示队列中在该任务之前等待执行的任务数。 131 | 4. 我们只允许一个漏线程,也就是把 `wip` 从 0 增加到 1 的线程。 132 | 5. 如果当前线程赢得了漏权利,那我们就把 worker 自己提交到 `Executor` 上,并在 `run()` 函数中从队列中取出任务执行。 133 | 134 | 注意,即便我们持有了 `ExecutorService` 的引用,我们也不能把这次提交的 `Future` 和任何 action 联系起来,所以取消任务只能通过间接的方式完成。 135 | 136 | 现在我们继续实现 `run()` 函数: 137 | 138 | ~~~ java 139 | @Override 140 | public void run() { 141 | do { 142 | if (isUnsubscribed()) { // (1) 143 | queue.clear(); 144 | return; 145 | } 146 | ScheduledAction sa = queue.poll(); // (2) 147 | if (sa != null && !sa.isUnsubscribed()) { 148 | sa.run(); // (3) 149 | } 150 | } while (wip.decrementAndGet() > 0); // (4) 151 | } 152 | ~~~ 153 | 154 | 漏的逻辑比较直观: 155 | 156 | 1. 我们先检查 worker 是否已经被取消请阅,如果已经取消,那我们就清空队列,并且返回。 157 | 2. 我们从队列中取出一个任务。 158 | 3. 由于在 `run()` 函数执行期间可能会删除任务,或者由于取消订阅而清空队列,所以我们需要检查是否为 null,以及是否已经被取消订阅。如果都没有,那我们就执行这个任务。 159 | 4. 我们递减 `wip`,直到为 0 就退出循环,此时我们就可以安全地重新调度这个 worker,并在 `Executor` 上执行漏任务了。 160 | 161 | 最后,最复杂的就是延迟调度的实现了 `schedule()`: 162 | 163 | ~~~ java 164 | @Override 165 | public Subscription schedule( 166 | Action0 action, 167 | long delayTime, 168 | TimeUnit unit) { 169 | 170 | if (delayTime <= 0) { 171 | return schedule(action); // (1) 172 | } 173 | if (isUnsubscribed()) { 174 | return Subscriptions.unsubscribed(); // (2) 175 | } 176 | 177 | ScheduledAction sa = 178 | new ScheduledAction(action); 179 | tracking.add(sa); 180 | sa.add(Subscriptions.create( 181 | () -> tracking.remove(sa))); // (3) 182 | 183 | ScheduledExecutorService schedex; 184 | if (exec instanceof ScheduledExecutorService) { 185 | schedex = (ScheduledExecutorService) exec; // (4) 186 | } else { 187 | schedex = CustomWorker.genericScheduler; // (5) 188 | } 189 | 190 | Future f = schedex.schedule(() -> { // (6) 191 | 192 | queue.offer(sa); // (7) 193 | 194 | sa.add(Subscriptions.create( 195 | () -> queue.remove(sa))); 196 | 197 | if (wip.getAndIncrement() == 0) { 198 | exec.execute(this); 199 | } 200 | 201 | }, delayTime, unit); 202 | 203 | sa.add(Subscriptions.create( 204 | () -> f.cancel(false))); // (8) 205 | 206 | return sa; 207 | } 208 | ~~~ 209 | 210 | 看上去和非延迟调度比较像,除了要想办法实现延迟: 211 | 212 | 1. 为了避免额外的开销,如果延迟非正,那我们就直接转发给非延迟的 `schedule()`。 213 | 2. 如果 worker 已经被取消订阅,我们直接返回表示已经取消订阅的 `Subscription`。 214 | 3. 我们把任务包装为 `ScheduledAction`,加入到 `tracking` 中,并且在取消订阅时从 `tracking` 中移除。 215 | 4. 我们需要一个支持延迟执行的 `Executor`,所以我们检查一下我们的 `Executor` 是否支持这一功能(`ScheduledExecutorService`)。 216 | 5. 如果不支持,那我们就要自己来实现延迟执行,例如使用[本系列第二篇](/AdvancedRxJava/2016/08/19/schedulers-2/index.html){:target="_blank"}中的 `CustomWorker.genericScheduler`。 217 | 6. 有了支持延迟执行的 service 之后,我们直接延迟执行任务即可。 218 | 7. 当延迟任务执行时,我们把任务加入到队列中,并在取消订阅时将任务从队列中移除,递增 `wip`,并且在 0 到 1 的递增情况下进入漏循环。 219 | 8. 我们需要保证在取消订阅时,也要取消掉我们延迟执行的任务(通过返回的 `Future`)。 220 | 221 | 让我们实验一下 `ExecutorScheduler`: 222 | 223 | ~~~ java 224 | ExecutorService exec = Executors.newFixedThreadPool(3); 225 | try { 226 | Scheduler s = new ExecutorScheduler(exec); 227 | 228 | Observable source = Observable.just(1) 229 | .delay(500, TimeUnit.MILLISECONDS, s) 230 | .doOnNext(v -> { 231 | try { 232 | Thread.sleep(1000); 233 | } catch (InterruptedException ex) { 234 | ex.printStackTrace(); 235 | } 236 | System.out.println(Thread.currentThread()); 237 | }); 238 | 239 | TestSubscriber ts1 = new TestSubscriber<>(); 240 | TestSubscriber ts2 = new TestSubscriber<>(); 241 | TestSubscriber ts3 = new TestSubscriber<>(); 242 | 243 | source.subscribe(ts1); 244 | source.subscribe(ts2); 245 | source.subscribe(ts3); 246 | 247 | ts1.awaitTerminalEvent(); 248 | ts1.assertNoErrors(); 249 | ts1.assertValue(1); 250 | 251 | ts2.awaitTerminalEvent(); 252 | ts2.assertNoErrors(); 253 | ts2.assertValue(1); 254 | 255 | ts3.awaitTerminalEvent(); 256 | ts3.assertNoErrors(); 257 | ts3.assertValue(1); 258 | } finally { 259 | exec.shutdown(); 260 | } 261 | ~~~ 262 | 263 | 我们将得到这样的输出: 264 | 265 | ~~~ bash 266 | Thread[pool-1-thread-3,5,main] 267 | Thread[pool-1-thread-1,5,main] 268 | Thread[pool-1-thread-2,5,main] 269 | ~~~ 270 | 271 | _译者注:这个例子是这样的,`delay` 操作传入了我们实现的 `scheduler`,每次 subscribe 时,都会创建一个新的 worker,我们连续 subscribe 了 3 次,每次执行时都会 sleep 1 秒,所以我们每次 subscribe 都会执行在不同的线程上。但这个例子并没有验证在同一个 worker 上进行多次 schedule 能保证串行执行,所以这个例子并不恰当。_ 272 | 273 | ## 总结 274 | 275 | 在这篇关于 `Scheduler` 的文章中,我讲解了在底层 `Executor` 是多线程时,如何保证非延迟调度串行按序执行。 276 | 277 | 在下一篇中,我将讲解如何在不支持像 `Future` 这样的取消机制的框架中(例如 GUI 框架的事件循环),让 `Scheduler` 和它们很好地一起工作。 278 | -------------------------------------------------------------------------------- /_posts/2017-03-19-comparison-of-reactive-streams-part-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Reactive Stream 各实现的对比(一) 4 | tags: 5 | - Reactive Stream 6 | --- 7 | 8 | 原文 [Comparison of Reactive-Streams implementations (part 1)](http://akarnokd.blogspot.com/2015/10/comparison-of-reactive-streams.html){:target="_blank"}。 9 | 10 | ## 介绍 11 | 12 | [Reactive-Streams](https://github.com/reactive-streams/reactive-streams-jvm) 最近在并发/并行技术圈越来越知名,也出现了好几个不同的实现,最值得关注的包括:[Akka-Streams](https://github.com/akka/akka/tree/master/akka-stream/src),[Project Reactor](https://github.com/reactor/reactor) 和 [RxJava 2.0](https://github.com/ReactiveX/RxJava/tree/2.x)。 13 | 14 | 在本文中,我将展示如何利用这些库实现简单的数据流,并且用 JMH 进行性能测试。为了对比的完整性,我还对 RxJava 1.0.14,Java 以及 `j.u.c.Stream` 进行了测试。 15 | 16 | 在第一部分中,我将用下面的测例,对比 4 个库的同步行为: 17 | 18 | 1. 1 到 (1,1000,1,000,000)的 `range`; 19 | 2. 对(1)中的数据流使用 `flatMap` 操作,把每个数据转换为只有一个数据的数据流; 20 | 3. 对(1)中的数据流使用 `flatMap` 操作,把每个数据转换为有两个数据的数据流; 21 | 22 | 运行环境: 23 | 24 | + Gradle 2.8 25 | + JMH 1.11.1 26 | - Threads: 1 27 | - Forks: 1 28 | - Mode: Throughput 29 | - Unit: ops/s 30 | - Warmup: 5, 1s each 31 | - Iterations: 5, 2s each 32 | + i7 4790 @ 3.5GHz stock settings CPU 33 | + 16GB DDR3 @ 1600MHz stock RAM 34 | + Windows 7 x64 35 | + Java 8 update 66 x64 36 | 37 | ## RxJava 38 | 39 | 我们首先看看 RxJava,添加 RxJava 1.x 的依赖: 40 | 41 | ~~~ gradle 42 | compile 'io.reactivex:rxjava:1.0.14' 43 | ~~~ 44 | 45 | 添加 RxJava 2.x 的依赖: 46 | 47 | ~~~ gradle 48 | repositories { 49 | mavenCentral() 50 | 51 | maven { url 'https://oss.jfrog.org/libs-snapshot' } 52 | } 53 | 54 | compile 'io.reactivex:rxjava:2.0.0-DP0-SNAPSHOT' 55 | ~~~ 56 | 57 | 遗憾的是,上述两个版本不能同时依赖,可以注释掉其中一个,或者使用我的 backport: 58 | 59 | ~~~ gradle 60 | compile 'com.github.akarnokd:rxjava2-backport:2.0.0-RC1' 61 | ~~~ 62 | 63 | 设置好依赖之后,我们看代码: 64 | 65 | ~~~ java 66 | @Params({"1", "1000", "1000000"}) 67 | int times; 68 | //... 69 | 70 | Observable range = Observable.range(1, times); 71 | 72 | Observable rangeFlatMapJust = range 73 | .flatMap(Observable::just); 74 | 75 | Observable rangeFlatMapRange = range 76 | .flatMap(v -> Observable.range(v, 2)); 77 | ~~~ 78 | 79 | 不同版本的 RxJava 代码是一样的,只是 import 包名不一样。 80 | 81 | 我们利用 `LatchedObserver` 接收数据,它可以在各个库的测试中复用: 82 | 83 | ~~~ java 84 | public class LatchedObserver extends Observer { 85 | public CountDownLatch latch = new CountDownLatch(1); 86 | private final Blackhole bh; 87 | public LatchedRSObserver(Blackhole bh) { 88 | this.bh = bh; 89 | } 90 | @Override 91 | public void onComplete() { 92 | latch.countDown(); 93 | } 94 | @Override 95 | public void onError(Throwable e) { 96 | latch.countDown(); 97 | } 98 | @Override 99 | public void onNext(T t) { 100 | bh.consume(t); 101 | } 102 | } 103 | ~~~ 104 | 105 | 由于数据流是同步的,所以我们这里实际上用不到 `latch`,只需要订阅即可: 106 | 107 | ~~~ java 108 | @Benchmark 109 | public void range(Blackhole bh) { 110 | range.subscribe(new LatchedObserver(bh)); 111 | } 112 | ~~~ 113 | 114 | 让我们看看 1.x 和 2.x 的结果: 115 | 116 | ![](https://imgs.piasy.com/2017-03-19-bench_rxjava.png) 117 | 118 | 上面是我的 [JMH 对比工具](https://github.com/akarnokd/jmh-compare-gui) 的一张截图,它能把结果用不同的颜色展示出来:绿色表示比基准线要好,红色则比基准线差。浅色表示差距在 +/- 3%,深色表示在 +/- 10%。 119 | 120 | _本文所有的截图中,数字越大表示性能越好。把 times 和结果数字相乘,就得到了事件个数。在这幅图中,times = 1,000,000 时,发射了将近 253M 个数据。_ 121 | 122 | 我们可以看出,RxJava 2.x 性能高出了一截,但有两个 RangeFlatMapJust 的例子除外,为什么会这样?我们分析一下。 123 | 124 | 性能的**提升**来自于 RxJava 2.x `subscribe()` 的开销比 1.x 更小。在 1.x 中我们订阅时,Subscriber 会被包装为 SafeSubscribe,而且当 Producer 被设置时,会发生一次小的“仲裁”。据我所知,JIT 在 1.x 中会尽可能消除掉内存分配和同步操作,但这个仲裁操作无法被移除,这样就会多执行一些 CPU 指令。而在 2.x 中,没有任何包装以及仲裁。 125 | 126 | 性能更差的情形,则是由于两个版本使用的串行访问方式不同:1.x 使用的是基于 synchronized 的发射者循环,而 2.x 则是用的基于原子操作的队列漏。前者可以被 JIT 消除掉(因为这里是单线程测试),而后者则不能消除,所以每次发射数据都将有大约 17ns 的开销。我打算对 2.x 做一次全面的性能分析,所以这个问题不会存在太久。 127 | 128 | 总结一下,RxJava 在高性能和易用性方面都做得很好,为什么要提到易用性?请见下文。 129 | 130 | ## Project Reactor 131 | 132 | Project Reactor 支持 Reactive-Streams 规范,也提供了类似于 RxJava 的流式 API。 133 | 134 | 我之前简单测试了它的 2.0.5-RELEASE 版本,但现在我要用最新的版本进行测试: 135 | 136 | ~~~ gradle 137 | repositories { 138 | mavenCentral() 139 | 140 | maven { url 'http://repo.spring.io/libs-snapshot' } 141 | } 142 | 143 | compile 'io.projectreactor:reactor-stream:2.1.0.BUILD-SNAPSHOT' 144 | ~~~ 145 | 146 | 这样可以确保能够包含他们最新的性能提升。 147 | 148 | 测试代码看起来很相近: 149 | 150 | ~~~ java 151 | Stream range = Streams.range(1, times); 152 | 153 | Stream rangeFlatMapJust = raRange.flatMap(Streams::just); 154 | 155 | Stream rangeFlatMapRange = raRange 156 | .flatMap(v -> Streams.range(v, 2)); 157 | ~~~ 158 | 159 | 注意 `Streams.range()` 的 API 发生了一点变化,2.0.5 中它接受的是 start+end 参数(闭区间),现在变成了 start+count,和 RxJava 的 range 一致。 160 | 161 | 我们仍用 LatchedObserver 接收数据,测试结果如下: 162 | 163 | ![](https://imgs.piasy.com/2017-03-19-bench_reactor.png) 164 | 165 | 上图中,reactor2 代表了 2.1.0 snapshot,reactor1 代表了 2.0.5 release。显然 Reactor 通过减少操作符的开销(大约优化了 10 倍)极大地提升了性能。 166 | 167 | 但 RangeFlatMapJust 测试中有一个奇怪的现象:RxJava 1.x 和 Reactor 2.1.0 都比 RxJava 2.x 的性能要好,而且它俩性能差不多。why? 168 | 169 | 在 flatMap 操作符中,RxJava 1.x 是基于 synchronized 的发射者循环,在单线程时,可以被 JIT 移除,从而降低开销,2.x 则是基于原子操作的队列漏,在快路径上的两次原子操作无法被 JIT 移除。 170 | 171 | 那我们看看 Reactor 是怎么做的。它的 flatMap 是用 FlatMapOperator 实现的,而它的实现和 RxJava 2.x 几乎一致!甚至它们都有同样的 bug! 172 | 173 | 当然 bug 是玩笑,它们有几处细微的差别,所以我们重点关注快路径的差别,找出 Reactor 吞吐量高出 4~8M 的原因。 174 | 175 | `doNext()` 看起来完全一样:如果数据源是 Supplier,那就直接取出数据,不用订阅,然后在 tryEmit() 中发射数据。 176 | 177 | **可能的 bug:如果这条路径发生了错误,进入到了 reportError(),那代码将会继续执行,Publisher 将会被订阅。** 178 | 179 | **可能的 bug:在 RxJava 2.x 中,我们始终会在调用用户接口的时候用 try-catche 包起来,所以用户代码的错误都是就地处理的。而在 Reactor 的实现中,doNext 里面并没有这样做(有可能在调用链的上游某处做了处理)。** 180 | 181 | tryEmit() 几乎完全一样,但有一处至关重要的差别:它把请求做了打包处理,而不是逐个向上游请求。有意思! 182 | 183 | ~~~ java 184 | if (maxConcurrency != Integer.MAX_VALUE && !cancelled 185 | && ++lastRequest == limit) { 186 | lastRequest = 0; 187 | subscription.request(limit); 188 | } 189 | ~~~ 190 | 191 | 在内部 Subscriber 的实现中,RxJava 2.x 和 Reactor 都实现了同样的打包逻辑(尽管上面的测例并没有体现这一点)。**Project Reactor 干得漂亮!** 192 | 193 | 在 RangeFlatMapRange 中,我们并没有进入快路径,Reactor 的性能稍逊一些,尽管它的 flatMap 实现是一样的。原因在于 Reactor 的 range 操作每秒就要少产生 100M 个数据。 194 | 195 | 沿着引用链条,我们发现了大量的包装和概括(generalization),但它们对每个 Subscriber 只会有一次,所以它在 times = 1000000 时不会是主要原因。 196 | 197 | 原因在于 Reactor 的 range() 实现类似于 RxJava 2.x 的 generator(例如 SyncOnSubscribe)。ForEachBiConsumer 看起来很简洁,但我依然能看出几处不足之处: 198 | 199 | + 使用了原子操作,这让 JIT 优化的代码必须从缓存读取数据,而不能利用寄存器。requestConsumer 可以在循环之前先读到局部变量中。 200 | + 尽可能多的使用 `==` 或者 `!=` 操作,因为其他比较操作在 x86 中性能差一些。 201 | + 原子递减操作比较耗时(~10 ns),它可以延后:一旦已知的请求数量已经处理完毕,我们可以先读取请求量,看看是否在我们处理的同时来了新的请求。如果有新的请求,那我们就可以继续发射,否则我们才从请求量中减去已经发射的数量。 202 | 203 | RxJava 的 range 现在还没有实现后面的几点;HotSpot 的寄存器分配现在看起来还比较混乱:(x64 平台的)寄存器超量导致了太多的局部变量和性能下降。实现后面几点需要更多的局部变量,这有可能让性能更差。 204 | 205 | 总的来说,Reactor 在各个版本中已经越来越好了,尤其是当它采用了和 RxJava 2.x 相同的结构和算法之后 :) 206 | 207 | ## Akka-Streams 208 | 209 | 我认为 Akka-Streams 是上述库中广告做得最多的。它背后是一家商业公司,而且移植自 Scala,它可能会有什么问题? 210 | 211 | _译者注:从这里我们可以看出作者对 Akka 有那么一点点鄙视。原文中作者写出 Akka 的测例也是费了半天功夫,甚至上 StackOverflow 提了问题,因为对 Scala 不熟,其中的曲折故事这里就不展开了,感兴趣可以[阅读原文](http://akarnokd.blogspot.com/2015/10/comparison-of-reactive-streams.html)。_ 212 | 213 | ![](https://imgs.piasy.com/2017-03-19-bench_akka.png) 214 | 215 | 注意,由于 Akka 不支持同步模式,我们必须用各种办法使得可以测试,我们可以给结果乘以 5~10。 216 | 217 | 我也不知道这里是怎么回事,有些数据慢了 100x。 218 | 219 | 总结一下,我对 Akka-Streams 非常失望,为了运行一个简单的数据流,我不得不大费周章,而且性能上还任重道远。 220 | 221 | ## Java 和 `j.u.c.Stream` 222 | 223 | 仅仅是作为参考,让我们看看纯用 Java for 循环以及 `j.u.c.Stream` 的性能。 224 | 225 | 普通 Java for 循环代码如下: 226 | 227 | ~~~ java 228 | @Benchmark 229 | public void javaRange(Blackhole bh) { 230 | int n = times; 231 | for (int i = 0; i < n; i++) { 232 | bh.consume(i); 233 | } 234 | } 235 | 236 | @Benchmark 237 | public void javaRangeFlatMapJust(Blackhole bh) { 238 | int n = times; 239 | for (int i = 0; i < n; i++) { 240 | for (int j = i; j < i + 1; j++) { 241 | bh.consume(j); 242 | } 243 | } 244 | } 245 | 246 | @Benchmark 247 | public void javaRangeFlatMapRange(Blackhole bh) { 248 | int n = times; 249 | for (int i = 0; i < n; i++) { 250 | for (int j = i; j < i + 2; j++) { 251 | bh.consume(j); 252 | } 253 | } 254 | } 255 | ~~~ 256 | 257 | Stream 的测例写起来稍微复杂一些,因为 `j.u.c.Stream` 无法复用,我们每次都需要重新创建: 258 | 259 | ~~~ java 260 | @Benchmark 261 | public void streamRange(Blackhole bh) { 262 | values.stream().forEach(bh::consume); 263 | } 264 | 265 | @Benchmark 266 | public void streamRangeFlatMapJust(Blackhole bh) { 267 | values.stream() 268 | .flatMap(v -> Collections.singletonList(v).stream()) 269 | .forEach(bh::consume); 270 | } 271 | 272 | @Benchmark 273 | public void streamRangeFlatMapRange(Blackhole bh) { 274 | values.stream() 275 | .flatMap(v -> Arrays.asList(v, v + 1).stream()) 276 | .forEach(bh::consume); 277 | } 278 | ~~~ 279 | 280 | 最后,为了好玩,我们做一个 Stream 并发版的测试: 281 | 282 | ~~~ java 283 | @Benchmark 284 | public void pstreamRange(Blackhole bh) { 285 | values.parallelStream().forEach(bh::consume); 286 | } 287 | 288 | @Benchmark 289 | public void pstreamRangeFlatMapJust(Blackhole bh) { 290 | values.parallelStream() 291 | .flatMap(v -> Collections.singletonList(v).stream()) 292 | .forEach(bh::consume); 293 | } 294 | 295 | @Benchmark 296 | public void pstreamRangeFlatMapRange(Blackhole bh) { 297 | values.parallelStream() 298 | .flatMap(v -> Arrays.asList(v, v + 1).stream()) 299 | .forEach(bh::consume); 300 | } 301 | ~~~ 302 | 303 | 好了,让我们看看结果: 304 | 305 | ![](https://imgs.piasy.com/2017-03-19-bench_java.png) 306 | 307 | 除了在几种并发的情形下,Java/Stream 的性能非常突出!在并发的情形下,我推测 forEach 把所有的并发操作同步到单个线程了,这就丢掉了所有的优势。 308 | 309 | 总结一下,如果我们有同步的任务,先试试基本的 Java 循环。 310 | 311 | ## 总结 312 | 313 | 在本文中,我在同步场景下,对三个 Reactive-Streams 库的易用性和性能进行了对比测试。相比于纯 Java,RxJava 和 Reactor 都做得不错,但 Akka-Streams 用起来则相当麻烦,而且性能也令人失望至极。 314 | 315 | ![](https://imgs.piasy.com/2017-03-19-bench_all.png) 316 | 317 | 但是也不排除 Akka-Streams 在下篇的异步场景中能有所表现。 318 | -------------------------------------------------------------------------------- /_posts/2016-09-25-the-reactive-streams-api-part-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Reactive-Streams API(二):SingleSubscription,SingleDelayedSubscription 和 RangeSubscription 4 | tags: 5 | - Reactive Stream 6 | --- 7 | 8 | 原文 [The Reactive-Streams API (part 2)](http://akarnokd.blogspot.com/2015/06/the-reactive-streams-api-part-2.html){:target="_blank"} 9 | 10 | ## 介绍 11 | 12 | 在本文中,我将把我们以前的 `SingleProducer` 和 `SingleDelayedProducer` 移植到基于 reactive-streams 的 `Subscription`。 13 | 14 | 首先,很多人可能认为这个转换过程很麻烦,但幸运的是,如果我们已经想清楚了在 `rx.Producer` 中如何实现 `request()`,那我们基本上就已经完成了 75% 了。剩下的 25% 来自于我们要把 `rx.Subscriber.isUnsubscribed()` 中的逻辑转移到 `request()` 中,因为 `org.reactivestreams.Subscriber` 中没有 `isUnsubscribed()`(其他资源管理类的接口都没有这个方法了)。 15 | 16 | ## `SingleSubscription` 17 | 18 | 由于 `SingleSubscription` 本身并不复杂,这里我就直接一步到位: 19 | 20 | ~~~ java 21 | import org.reactivestreams.*; 22 | 23 | public final class SingleSubscription 24 | extends AtomicBoolean implements Subscription { 25 | private static final long serialVersionUID = 1L; 26 | 27 | final T value; // (1) 28 | final Subscriber child; 29 | volatile boolean cancelled; // (2) 30 | 31 | public SingleSubscription(T value, 32 | Subscriber child) { // (3) 33 | this.value = Objects.requireNonNull(value); 34 | this.child = Objects.requireNonNull(child); 35 | } 36 | @Override 37 | public void request(long n) { 38 | if (n <= 0) { 39 | throw new IllegalArgumentException( 40 | "n > 0 required"); // (4) 41 | } 42 | if (compareAndSet(false, true)) { 43 | if (!cancelled) { // (5) 44 | child.onNext(value); 45 | if (!cancelled) { 46 | child.onComplete(); 47 | } 48 | } 49 | } 50 | } 51 | @Override 52 | public void cancel() { 53 | cancelled = true; // (6) 54 | } 55 | } 56 | ~~~ 57 | 58 | 就是这样!这里我向大家展示了把 `Producer` 的实现迁移到 reactive-streams `Subscription` 并不需要太多的工作。但这里还是有几点值得一提: 59 | 60 | 1. 我们需要在成员变量中保存将要发出的值,以及下游 subscriber。 61 | 2. 然而由于 RS 中没有了 `isUnsubscribed()`,而且取消订阅也变成了 `cancel()`,所以我们需要在一个 `volatile` 变量中记录是否已经取消订阅。如果你还记得的话,我说过我们无法预知 `request()` 和 `cancel()` 的调用情况,所以我们需要保证它们的线程安全性。 62 | 3. 由于 RS 不允许 `null`,我们在构造函数中就检查错误。 63 | 4. 我的“Let them throw!”哲学告诉我们非正请求数量是编程错误,我们应该抛出 `IllegalArgumentException`。 64 | 5. 由于没有 `child.isUnsubscribed()` 函数了,我们只能检查我们的 `cancelled` 变量。 65 | 6. 为了保证取消的幂等性,我们只是安全的更改 `cancelled` 变量。 66 | 67 | ## `SingleDelayedSubscription` 68 | 69 | `SingleSubscription` 都这么简单了,`SingleDelayedSubscription` 又能复杂到哪里去呢? 70 | 71 | ~~~ java 72 | public final class SingleDelayedSubscription 73 | extends AtomicInteger implements Subscription { 74 | private static final long serialVersionUID = -1L; 75 | 76 | T value; 77 | final Subscriber child; 78 | 79 | static final int CANCELLED = -1; // (1) 80 | static final int NO_VALUE_NO_REQUEST = 0; 81 | static final int NO_VALUE_HAS_REQUEST = 1; 82 | static final int HAS_VALUE_NO_REQUEST = 2; 83 | static final int HAS_VALUE_HAS_REQUEST = 3; 84 | 85 | public SingleDelayedSubscription(Subscriber child) { 86 | this.child = Objects.requireNonNull(child); 87 | } 88 | @Override 89 | public void request(long n) { 90 | if (n <= 0) { 91 | throw new IllegalArgumentException("n > 0 required"); 92 | } 93 | for (;;) { 94 | int s = get(); 95 | if (s == NO_VALUE_HAS_REQUEST 96 | || s == HAS_VALUE_HAS_REQUEST 97 | || s == CANCELLED) { // (2) 98 | return; 99 | } else if (s == NO_VALUE_NO_REQUEST) { 100 | if (!compareAndSet(s, NO_VALUE_HAS_REQUEST)) { 101 | continue; 102 | } 103 | } else if (s == HAS_VALUE_NO_REQUEST) { 104 | if (compareAndSet(s, HAS_VALUE_HAS_REQUEST)) { 105 | T v = value; 106 | value = null; 107 | child.onNext(v); 108 | if (get() != CANCELLED) { // (3) 109 | child.onComplete(); 110 | } 111 | } 112 | } 113 | return; 114 | } 115 | } 116 | 117 | public void setValue(T value) { 118 | Objects.requireNonNull(value); 119 | for (;;) { 120 | int s = get(); 121 | if (s == HAS_VALUE_NO_REQUEST 122 | || s == HAS_VALUE_HAS_REQUEST 123 | || s == CANCELLED) { // (4) 124 | return; 125 | } else if (s == NO_VALUE_NO_REQUEST) { 126 | this.value = value; 127 | if (!compareAndSet(s, HAS_VALUE_NO_REQUEST)) { 128 | continue; 129 | } 130 | } else if (s == NO_VALUE_HAS_REQUEST) { 131 | if (compareAndSet(s, HAS_VALUE_HAS_REQUEST)) { 132 | child.onNext(value); 133 | if (get() != CANCELLED) { // (5) 134 | child.onComplete(); 135 | } 136 | } 137 | } 138 | return; 139 | } 140 | } 141 | 142 | @Override 143 | public void cancel() { 144 | int state = get(); 145 | if (state != CANCELLED) { // (6) 146 | state = getAndSet(CANCELLED); 147 | if (state != CANCELLED) { 148 | value = null; 149 | } 150 | } 151 | } 152 | } 153 | ~~~ 154 | 155 | 看起来和原来的状态机非常类似,但是多了一个 `CANCELLED` 状态,我们无需在 `onNext` 之前检查状态不为 `CANCELLED`,因为 CAS 操作隐含了这一条件,但我们应该在 `onComplete()` 之前进行检查。 156 | 157 | 为什么我们不使用一个 `volatile boolean` 来记录是否取消呢?其实完全可以。这种选择仅仅是出于个人偏好:增加一个成员变量,或者是扩展一个新状态。我主要是想要展示一下后者怎么实现。 158 | 159 | ## `RangeSubscription` 160 | 161 | 我并不打算在这里把以前所有的 `Producer` 都改写为 `Subscription`,但我这里还想展示一个包括取消状态的状态机例子: 162 | 163 | ~~~ java 164 | public final class RangeSubscription 165 | extends AtomicLong implements Subscription { 166 | private static final long serialVersionUID = 1L; 167 | 168 | final Subscriber child; 169 | int index; 170 | final int max; 171 | 172 | static final long CANCELLED = Long.MIN_VALUE; // (1) 173 | 174 | public RangeSubscription( 175 | Subscriber child, 176 | int start, int count) { 177 | this.child = Objects.requireNonNull(child); 178 | this.index = start; 179 | this.max = start + count; 180 | } 181 | @Override 182 | public void request(long n) { 183 | if (n <= 0) { 184 | throw new IllegalArgumentException( 185 | "n > required"); 186 | } 187 | long r; 188 | for (;;) { 189 | r = get(); 190 | if (r == CANCELLED) { // (2) 191 | return; 192 | } 193 | long u = r + n; 194 | if (u < 0) { 195 | u = Long.MAX_VALUE; 196 | } 197 | if (compareAndSet(r, u)) { 198 | break; 199 | } 200 | } 201 | if (r != 0L) { // (p1) 202 | return; 203 | } 204 | for (;;) { 205 | r = get(); 206 | if (r == CANCELLED) { // (3) 207 | return; 208 | } 209 | int i = index; 210 | int m = max; 211 | long e = 0; 212 | while (r > 0L && i < m) { // (p2) 213 | child.onNext(i); 214 | if (get() == CANCELLED) { // (4) 215 | return; 216 | } 217 | i++; 218 | if (i == m) { 219 | child.onComplete(); 220 | return; 221 | } 222 | r--; 223 | e++; 224 | } 225 | index = i; 226 | if (e != 0) { 227 | for (;;) { 228 | r = get(); 229 | if (r == CANCELLED) { // (5) 230 | return; 231 | } 232 | long u = r - e; 233 | if (u < 0) { 234 | throw new IllegalStateException( 235 | "more produced than requested!"); 236 | } 237 | if (compareAndSet(r, u)) { 238 | break; 239 | } 240 | } 241 | } 242 | if (r <= 0L) { // (p3) 243 | break; 244 | } 245 | } 246 | } 247 | @Override 248 | public void cancel() { 249 | if (get() != CANCELLED) { // (6) 250 | getAndSet(CANCELLED); 251 | } 252 | } 253 | } 254 | ~~~ 255 | 256 | 为了简洁起见,我省略了快速路径的逻辑。剩下的部分和原来的 `RangeProducer` 类似,但是取消状态被合并进了计数状态中,我们几乎需要在所有的地方(1~5)重新读出计数并和 `CANCELLED` 对比。注意发射计数再也不能用 `getAndAdd()` 了,直接递增可能会覆盖掉 `CANCELLED` 状态。最后在取消时使用 `getAndSet()` 可以保证幂等性。 257 | 258 | _译者注:这一段代码还是很复杂的,即便我之前翻译过 `RangeProducer` 的实现,看起来依然需要一些思考,所以这里进行一些分析(对应于上面的 p 标号):_ 259 | 260 | 1. 当我们成功更新请求计数之后,只有从 0 开始增加请求计数的线程可以进入后面的发射循环。发射过程会递减请求计数,当请求处理完毕之后(请求计数重新变为 0),下次的请求调用才有可能进入发射循环。 261 | 2. 这里发射循环有两个条件,一是有未处理的请求(`r > 0`),而是发射数据没有超出范围(`i < m`),这两者很可能是不同的。此外,如果发射数据已经超出了范围,而且请求计数也递减为 0 了,那后续的请求仍然能通过(p1)的检查,但不会进入发射循环,因为通不过 `i < m` 的检查。 262 | 3. 这里是请求处理完毕,但发射数据没有超出范围的退出路径。 263 | 264 | ## 总结 265 | 266 | 在本文中,我展示了两种把 `rx.Producer` 改写为 RS `Subscription` 的方式(boolean 成员或者新状态),它们都能保证取消行为的正确性。对它们进行取舍需要进行权衡:boolean 成员会增加对象大小,新状态会增加算法复杂性。 267 | 268 | 下一篇文章中,我将介绍如何处理 RS 中缺失的另一个 `rx.Subscriber` 特性:用 `add(rx.Subscriber)` 把资源和下游 subscriber 结合起来。 269 | -------------------------------------------------------------------------------- /_posts/2016-09-30-the-reactive-streams-api-part-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Reactive-Streams API(三):资源管理与 TakeUntil 4 | tags: 5 | - Reactive Stream 6 | --- 7 | 8 | 原文 [The Reactive-Streams API (part 3)](http://akarnokd.blogspot.com/2015/06/the-reactive-streams-api-part-3.html){:target="_blank"} 9 | 10 | ## 介绍 11 | 12 | 在本文中,我将讲解如何把 `rx.Subscriber` 管理资源的能力移植到 Reactive-Streams API 中。但是由于 RS 并未明确任何资源管理方面的要求,所以我们需要引入(把 `rx.Subscription` 重命名)我们自己的容器类型,并把它加入到 RS 的 `Subscriber` 的取消逻辑中。 13 | 14 | ## Subscription vs. Subscription 15 | 16 | 为了避免造成困惑,RxJava 2.0 把 `XXXSubscription` 替换为了 `XXXDisposable`,我不会在这里详细介绍这些类,但是会讲几个资源管理的基本接口: 17 | 18 | ~~~ java 19 | interface Disposable { 20 | boolean isDisposed(); 21 | void dispose(); 22 | } 23 | 24 | interface DisposableCollection extends Disposable { 25 | boolean add(Disposable resource); 26 | boolean remove(Disposable resource); 27 | boolean removeSilently(Disposable resource); 28 | 29 | void clear(); 30 | boolean hasDisposables(); 31 | boolean contains(Disposable resource); 32 | } 33 | ~~~ 34 | 35 | 使用的规则是一样的:线程安全、幂等性。 36 | 37 | ## `DisposableSubscription` 38 | 39 | 加入资源管理最基本的方式就是对 `Subscription` 进行一次包装,拦截 `cancel()` 调用并且调用底层容器类的 `dispose()`: 40 | 41 | ~~~ java 42 | public final class DisposableSubscription 43 | implements Disposable, Subscription { 44 | final Subscription actual; 45 | final DisposableCollection collection; 46 | public DisposableSubscription( 47 | Subscription actual, 48 | DisposableCollection collection) { 49 | this.actual = Objects.requireNonNull(actual); 50 | this.collection = Objects.requireNonNull(collection); 51 | } 52 | public boolean add(Disposable resource) { 53 | return collection.add(resource); 54 | } 55 | public boolean remove(Disposable resource) { 56 | return collection.remove(resource); 57 | } 58 | public boolean removeSilently(Disposable resource) { 59 | return collection.remove(resource); 60 | } 61 | @Override 62 | public boolean isDisposed() { 63 | return collection.isDisposed(); 64 | } 65 | @Override 66 | public void dispose() { 67 | cancel(); 68 | } 69 | 70 | @Override 71 | public void cancel() { 72 | collection.dispose(); 73 | actual.cancel(); 74 | } 75 | @Override 76 | public void request(long n) { 77 | actual.request(n); 78 | } 79 | } 80 | ~~~ 81 | 82 | 由于 `DisposableSubscription` 也实现了 `Disposable` 接口,所以它也可以被加入到容器中,构成一个复杂的 dispose 网络。但是绝大多数情况下,我们都希望避免额外的内存分配,因此,上面的这些代码可能会被融入到其他的类型中,例如在 `lift()` 调用中创建的 `Subscriber` 类型。 83 | 84 | 如果你熟悉 RxJava 的规范,以及[操作符实现的陷阱之一](/AdvancedRxJava/2016/05/29/pitfalls-of-operator-implementations/#unsubscribing-the-downstream){:target="_blank"},那就不应该取消订阅下游,因为这可能会导致资源的提前释放。 85 | 86 | (_这也是目前 RxAndroid 的 `LifecycleObservable` 的一个 bug,当我们在中间插入一个类似于 `takeUntil()` 的操作符时,它不会向下游发送 `onCompleted()`,而是取消订阅了下游。_) 87 | 88 | 在 RS 中,取消订阅下游实际上是不会发生的。每一层都要么只能原封不动的转发 `Subscription`(因此无法添加资源),要么只能把它包装为 `DisposableSubscription` 这样的类型,然后依然把下游当做一个 `Subscription` 进行转发。如果你在这一层调用了 `cancel()`,你是无法调用包装 Subscriber 的类的 `cancel()` 的。 89 | 90 | 当然,你非要搞破坏那肯定是可以的,但 RS 比 RxJava 做了更多的努力,而且原则是不变的:不应该 cancel/dispose 下游的资源,或者在操作符链条中共享资源。 91 | 92 | ## `TakeUntil` 93 | 94 | 现在让我们看看怎么实现能够管理外部资源的 `takeUntil()` 操作符(我把代码拆分了一下,更方便阅读): 95 | 96 | ~~~ java 97 | public final class OperatorTakeUntil 98 | implements Operator { 99 | final Publisher other; 100 | public OperatorTakeUntil(Publisher other) { 101 | this.other = Objects.requireNonNull(other); 102 | } 103 | @Override 104 | public Subscriber call( 105 | Subscriber child) { 106 | Subscriber serial = 107 | new SerializedSubscriber<>(child); 108 | 109 | SubscriptionArbiter arbiter = 110 | new SubscriptionArbiter(); // (1) 111 | serial.onSubscribe(arbiter); 112 | 113 | SerialDisposable sdUntil = new SerialDisposable(); // (2) 114 | SerialDisposable sdParent = new SerialDisposable(); // (3) 115 | ~~~ 116 | 117 | 到目前为止,看起来都和 RxJava 的实现类似:我们把 child 包装为一个 `SerializedSubscriber`,防止 `Publisher` 并行发出 `onError()` 和 `onCompleted()`。 118 | 119 | 我们创建了一个 `SubscriptionArbiter`(一个 `ProducerArbiter` 的变体),主要是考虑以下原因:假设我们正在 `call()` 函数中,另一个已经订阅的数据源发出了一个数据,那我们就需要把数据转发到 child Subscriber 中,然而在我们得到 `Subscription` 之前(_调用 `onSubscribe()` 之前_),我们是无法在其上调用 `onXXX` 函数的,所以我们只能等到操作符链条上调用 `onSubscribe()` 之后(_拿到 `Subscription` 之后_),才可以转发数据。我会在下一篇文章中更详细地讲解这个问题。 120 | 121 | 然而,由于取消订阅的机会在 `Subscriber` 那里(_我们调用 `Subscription.cancel()` 取消订阅,而我们会调用 `Subscriber.onSubscribe(Subscription)` 把 `Subscription` 交给 `Subscriber`_),我们需要把 `Subscription` 从子 `Subscriber` 传递到父 `Subscriber` 中(2),这样它们中的任意一个到达终止状态时,它都能 `cancel()` 另一个。由于子 Subscriber 可能比父 Subscriber 先收到 Subscription 和取消事件,我们也将需要反过来取消父 Subscriber(3)。 122 | 123 | ~~~ java 124 | // ... 125 | Subscriber parent = new Subscriber() { 126 | DisposableSubscription dsub; 127 | @Override 128 | public void onSubscribe(Subscription s) { 129 | DisposableSubscription dsub = 130 | new DisposableSubscription(s, 131 | new DisposableList()); // (1) 132 | dsub.add(sdUntil); // (2) 133 | sdParent.set(dsub); 134 | arbiter.setSubscription(dsub); // (3) 135 | } 136 | @Override 137 | public void onNext(T t) { 138 | serial.onNext(t); 139 | } 140 | @Override 141 | public void onError(Throwable t) { 142 | serial.onError(t); 143 | sdParent.cancel(); // (4) 144 | } 145 | @Override 146 | public void onComplete() { 147 | serial.onComplete(); 148 | sdParent.cancel(); 149 | } 150 | }; 151 | ~~~ 152 | 153 | 父 Subscriber 的实现略有不同,我们需要处理后来的 Subscription,并且建立好取消订阅的链条: 154 | 155 | 1. 我们创建了一个 `DisposableSubscription`,底层使用基于 List 的 Disposable 集合。 156 | 2. 我们把包装了 `Disposable`(指向另一个 `Subscription`) 的 `SerialDisposable` 加入到容器中。我们也把 `DisposableSubscription` 加入到 `sdParent` 中,这就让另一个 `Subscriber` 可以在 parent 开始之前结束自己。 157 | 3. 我们把包装好的对象加入到 `arbiter` 中。 158 | 4. 当错误事件发生时,我们要确保取消掉容器。而由于容器中包含了另一个 `Subscription`,所以整个事件流也会被取消。 159 | 160 | 最后我们需要为另一个事件流创建 `Subscriber`,并且确保它和 parent 连接起来: 161 | 162 | ~~~ java 163 | // ... 164 | Subscriber until = new Subscriber() { 165 | @Override 166 | public void onSubscribe(Subscription s) { 167 | sdUntil.set(Disposables.create(s::cancel)); // (1) 168 | s.request(Long.MAX_VALUE); 169 | } 170 | @Override 171 | public void onNext(Object t) { 172 | parent.onComplete(); // (2) 173 | } 174 | @Override 175 | public void onError(Throwable t) { 176 | parent.onError(t); 177 | } 178 | @Override 179 | public void onComplete() { 180 | parent.onComplete(); 181 | } 182 | }; 183 | 184 | this.other.subscribe(until); 185 | 186 | return parent; 187 | } 188 | } 189 | ~~~ 190 | 191 | 我们从另一个数据源接收到 `Subscription` 时,我们就把它包装为一个 `Disposable`(和现在 `Subscription.create()` 的做法一样)。由于 `Disposable` 的终结状态特性,即便我们的主要事件流在另一个流接收到 `Subscription` 之前就已经结束,包装的 `SerialDisposable` 依然会被取消,而这就会立即取消刚刚接收到的 `Subscription`。(_译者注:对于“终结状态特性”,不了解的朋友可以看看之前的文章:[Operator 并发原语: subscription-containers(一)](/AdvancedRxJava/2016/07/15/operator-concurrency-primitives-subscription-containers-1/#section){:target="_blank"}_) 192 | 193 | 注意,由于取消/释放资源的能力取决于时间和顺序,所以我们通常都需要为每个资源创建一个容器(例如 `sdParent`、`sdOther`),这样无论哪个 `Subscription` 在任何时候到达时,我们都能释放所有的资源。 194 | 195 | ## `TakeUntil` v2 196 | 197 | 如果仔细看看上面 `takeUntil()` 的实现,就会发现我们对各种 `Subscription` 进行了重新组织,我们其实可以理清 `Disposable` 导致的混乱: 198 | 199 | ~~~ java 200 | @Override 201 | // ... 202 | public Subscriber call(Subscriber child) { 203 | Subscriber serial = new SerializedSubscriber<>(child); 204 | 205 | SubscriptionArbiter sa = new SubscriptionArbiter(); // (1) 206 | 207 | DisposableSubscription dsub = 208 | new DisposableSubscription(sa, new DisposableList()); // (2) 209 | 210 | serial.onSubscribe(dsub); // (3) 211 | 212 | Subscriber parent = new Subscriber() { 213 | @Override 214 | public void onSubscribe(Subscription s) { 215 | dsub.add(Disposables.create(s::cancel)); // (4) 216 | sa.setSubscription(s); // (5) 217 | } 218 | // ... 219 | }; 220 | 221 | Subscriber until = 222 | new Subscriber() { 223 | @Override 224 | public void onSubscribe(Subscription s) { 225 | dsub.add(Disposables.create(s::cancel)); // (6) 226 | s.request(Long.MAX_VALUE); 227 | } 228 | // ... 229 | ~~~ 230 | 231 | 它的原理如下: 232 | 233 | 1. 我们创建了一个 `SubscriptionArbiter`。 234 | 2. 然后把它包装为一个 `DisposableSubscription`。 235 | 3. 然后把它推到下游。这种组合能保证任何的取消以及请求,都会积累到 arbiter 接收到一个“真正的” `Subscription` 时。 236 | 4. 一旦主流收到 `Subscription` 之后,我们就把它包装为一个 `Disposable` 并加入到 `dsub` 容器中。 237 | 5. 然后我们就更新 arbiter 的 `Subscription`:所有积累的请求以及取消操作都会“重放”到上游的 `Subscription` 上。 238 | 6. 当另一条流收到 `Subscription` 之后,我们也把它包装为一个 `Disposable` 并加入到 `dsub` 容器中。 239 | 240 | 最后 parent 会在它的 `onError()` 和 `onComplete()` 中调用 `dsub.dispose()` 了。 241 | 242 | 让我们梳理一下各种取消的路径: 243 | 244 | + 下游取消:下游会取消 `dsub`,`dsub` 会取消 arbiter,arbiter 会取消任何收到的 `Subscription`。 245 | + 主流结束:主流会取消 `dsub`,`dsub` 会取消 arbiter,arbiter 会取消上游的 `Subscription`。此外,`dsub` 也会取消其他存在的 `Subscription`。 246 | + 支流结束:支流会取消 `dsub`,`dsub` 会取消 arbiter。一旦主流收到 `Subscription` 之后,`dsub` 和 arbiter 都会立即取消这个 `Subscription`。 247 | 248 | ## 总结 249 | 250 | 在本文中,我讲解了怎么在 Reactive-Stream 的 `Subscriber` 和 `Subscription` 体系中实现资源管理,以及演示了如何实现一个 `takeUntil()` 操作符。 251 | 252 | 尽管看起来我们创建了和 RxJava 实现中同样多(甚至更多)的对象,但是很多操作符都不需要资源管理(例如 `take()`),甚至都不需要包装收到的 `Subscription` 对象(例如 `map()`)。 253 | 254 | 在下一篇(最后一篇)关于 RS API 的文章中,我将介绍我们对各种 arbiter 类型更大的需求(在本文的 `takeUntil()` 例子中已经有所涉及),因为我们必须给 `Subscriber` 设置一个 `Subscription` 并且还要保证取消功能的正常,此外即便数据源发生了变化我们也不能调用多次 `onSubscribe()`。 255 | -------------------------------------------------------------------------------- /_posts/2017-05-25-google-agera-vs-reactivex.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Google Agera vs. ReactiveX 4 | tags: 5 | - 对比点评 6 | --- 7 | 8 | 原文 [Google Agera vs. ReactiveX](http://akarnokd.blogspot.com/2016/04/google-agera-vs-reactivex.html){:target="_blank"}。 9 | 10 | ## 介绍 11 | 12 | 如果大家一直关注安卓开发圈内的事件,或者关注响应式编程相关的事件,那就知道最近 Google 搞了一件“大事情”:他们发布了专门针对安卓平台的响应式编程库 [Agera](https://github.com/google/agera)。(_译者注:其实 Agera 是一年前的事了,不过一年过去了,Agera 好像没什么动静了_)当然,我们需要仔细深入细节,才能准确了解 Agera 是怎么回事。 13 | 14 | “Google 制造”的意思其实是一个负责 Google Play 电影 APP 的团队制造。当然,说 Google 显然比说出这个团队的完整名字要震撼得多。当别人问我在哪里工作时我也会采取同样的策略:我会说在 **lab at the Hungarian Academy of Sciences** 而不是 **the Engineering and Management Intelligence Research Laboratory at the Institute for Computer Science and Control of the Hungarian Academy of Sciences**。(_不要在意原作者到底在哪里工作,都是浮云..._) 15 | 16 | 谁发布的并不重要,重要的是发布了啥,以及它和现有的响应式编程库是啥关系:[RxJava](https://github.com/ReactiveX/RxJava),[Reactor](https://github.com/reactor/reactor-core),[Akka-Streams](https://github.com/akka/akka/tree/master/akka-stream)。 17 | 18 | ## 核心 API 19 | 20 | Agera 库的基本思想是观察者模式:`Observable` 接收 `Updatable`,并通过 `update()` 接口通知新事件。之后搞清楚更新的内容就是 `Updatable` 自己的事了。这毫无疑问是一种依赖于 `update()` 调用副作用的响应式数据流。 21 | 22 | ~~~ java 23 | interface Updatable { 24 | void update(); 25 | } 26 | 27 | interface Observable { 28 | void addUpdatable(Updatable u); 29 | void removeUpdatable(Updatable u); 30 | } 31 | ~~~ 32 | 33 | 它们看起来人畜无害,而且很响应式对吧?然而不幸的是,它们有和 `java.util.Observable` 以及基于 `addListener`/`removeListener` 的 API 一样的问题,这种 API 我在[前文中归为第 0 代响应式编程库](/AdvancedRxJava/2017/05/01/operator-fusion-part-1/#section-2),可以看看[我在 GitHub 上的 issue 评论](https://github.com/google/agera/issues/20#issuecomment-212007539)。 34 | 35 | ### Agera Observable 36 | 37 | 这样一对接口的问题在于,每个想要通知 `Updatable` 后续更新的 `Observable`,都需要保存 `Updatable` 对象的引用,以便后续移除之: 38 | 39 | ~~~ java 40 | public final class DoOnUpdate implements Observable { 41 | final Observable source; 42 | 43 | final Runnable action; 44 | 45 | final ConcurrentHashMap map; 46 | 47 | public DoOnUpdate(Observable source, Runnable action) { 48 | this.source = source; 49 | this.action = action; 50 | this.map = new ConcurrentHashMap<>(); 51 | } 52 | 53 | @Override 54 | public void addUpdatable(Updatable u) { 55 | DoOnUpdatable wrapper = new DoOnUpdatable(u, action); 56 | if (map.putIfAbsent(u, wrapper) != null) { 57 | throw new IllegalStateException("Updatable already registered"); 58 | } 59 | source.addUpdatable(wrapper); 60 | } 61 | 62 | public void removeUpdatable(Updatable u) { 63 | DoOnUpdatable wrapper = map.remove(u); 64 | if (wrapper == null) { 65 | throw new IllegalStateException("Updatable already removed"); 66 | } 67 | source.removeUpdatable(wrapper); 68 | } 69 | 70 | static final class DoOnUpdatable { 71 | final Updatable actual; 72 | 73 | final Runnable run; 74 | 75 | public DoOnUpdatable(Updatable actual, Runnable run) { 76 | this.actual = actual; 77 | this.run = run; 78 | } 79 | 80 | @Override 81 | public void update() { 82 | run.run(); 83 | actual.update(); 84 | } 85 | } 86 | } 87 | ~~~ 88 | 89 | 而这导致了毫不相关的下游 `Updatable` 之间,在每个环节都存在竞争点。 90 | 91 | 的确,RxJava 的 [Subject](/AdvancedRxJava/2016/10/05/subjects-part-3/index.html) 和 [ConnectableObservable](/AdvancedRxJava/2017/03/03/connectableobservables-part-2/index.html) 中也存在类似的竞争点,但它们之后链起来的操作符之间是不存在竞争的。不幸的是,Reactive-Streams 规范当前版本是禁止 `Publisher` 存在类似的竞争的,但 RxJava,Rsc 和 Reactor 都忽略了这一点,这其实是过于保守了,我们也正在努力矫枉,以让规范更轻量。 92 | 93 | 第二个问题没这么严重,那就是我们没法添加同一个 `Updatable` 多次。首先因为我们用 `Map` 无法区分不同的“订阅关系”,其次,Agera 规范也要求在这种情况下抛出异常。当然,通常这种情况都不会发生,因为大多数末端消费者都只用一次。 94 | 95 | 第三个问题则严重一些了:当 `Updatable` 没有注册到 `Observable` 上时,抛出异常。这就导致了末端消费者和中间操作符移除 `Updatable` 的竞争,而且它俩之间必有一者会抛出异常。这正是现代响应式编程库的取消操作都是幂等的原因。 96 | 97 | 第四个问题则是,`addUpdatable` 和 `removeUpdatable` 之间理论上会存在竞争:下游操作符可能希望在上游调用 `addUpdatable` 之前就断开连接。这导致的结果就是 `removeUpdate` 抛出异常,但 `addUpdatable` 成功,使得数据流无论如何都会继续,而且会造成不必要的对象引用。 98 | 99 | ### Agera Updatable 100 | 101 | 让我们看看消费者这边的 API。`Updatable` 是个函数式接口,这样我们为 `Observable` 增加一个监听者就比较简洁: 102 | 103 | ~~~ java 104 | Observable source = ... 105 | 106 | source.addUpdatable(() -> System.out.println("Something happened")); 107 | ~~~ 108 | 109 | 足够简单,现在让我们移除一个监听者: 110 | 111 | ~~~ java 112 | source.removeUpdatable(() -> System.out.println("Something happened")); 113 | ~~~ 114 | 115 | 但这会抛出异常,因为这两个 lambda 表达式不是同一个实例。这是基于 `addListener`/`removeListener` API 的常见问题,解决办法就是把 lambda 表达式存起来再用: 116 | 117 | ~~~ java 118 | Updatable u = () -> System.out.println("Something happened"); 119 | 120 | source.addUpdatable(u); 121 | 122 | // ... 123 | 124 | source.removeUpdatable(u); 125 | ~~~ 126 | 127 | 确实只是一个小小的不便,但情况会恶化。如果我们有很多个 `Observable` 和 `Updatable` 怎么办?那我们就不得不记住谁注册到了谁上面,而且用变量保存起来。最初 Rx.NET 一个很好的点子就是通过一个接口来省去所有的这些不便: 128 | 129 | ~~~ java 130 | interface Removable extends Closeable { 131 | @Override 132 | void close(); // remove the necessity of try-catch around close() 133 | } 134 | 135 | public static Removable registerWith(Observable source, Updatable consumer) { 136 | source.addUpdatable(consumer); 137 | return () -> source.removeUpdatable(consumer); 138 | } 139 | ~~~ 140 | 141 | 当然,这里我们也要考虑到 `close()` 的幂等性: 142 | 143 | ~~~ java 144 | public static Removable registerWith(Observable source, Updatable consumer) { 145 | source.addUpdatable(consumer); 146 | final AtomicBoolean once = new AtomicBoolean(); 147 | return () -> { 148 | if (once.compareAndSet(false, true)) { 149 | source.removeUpdatable(consumer); 150 | } 151 | }); 152 | } 153 | ~~~ 154 | 155 | ### Agera MutableRepository 156 | 157 | `MutableRepository` 存有一个可变值,并且当这个值发生改变时调用 `update()` 通知已注册的 `Updatable`。这在某种程度上模拟了 `BehaviorSubject`,区别是新值不会传给下游(因为 `update()` 没有参数),下游需要通过 `get()` 主动获取: 158 | 159 | ~~~ java 160 | MutableRepository repo = Repositories.mutableRepository(0); 161 | 162 | repo.addUpdatable(() -> System.out.println("Value: " + repo.get()); 163 | 164 | new Thread(() -> { 165 | repo.accept(1); 166 | }).start(); 167 | ~~~ 168 | 169 | 当通过工厂方法创建 `MutableRepository` 时,它就具备了一个有趣的特性:`update()` 将在创建 `MutableRepository` 的线程被调用。(`Looper` 就像是每个线程私有的跳板调度器/ `Executor`,使得我们可以在指定的线程上执行代码,例如安卓的主线程) 170 | 171 | 这一特性就导致了下面这个有趣的情况: 172 | 173 | ~~~ java 174 | Set set = new HashSet<>(); 175 | 176 | MutableRepository repo = Repositories.mutableRepository(0); 177 | 178 | repo.addUpdatable(() -> set.add(repo.get())); 179 | 180 | new Thread(() -> { 181 | for (int i = 0; i < 100_000; i++) { 182 | repo.accept(i); 183 | } 184 | }).start(); 185 | 186 | Thread.sleep(20_000); 187 | 188 | System.out.println(set.size()); 189 | ~~~ 190 | 191 | [假设 20s 足够了](https://github.com/google/agera/issues/31),猜猜 `Set` 打印出来的大小是多少?我们可能会预期 `Set` 里面有所有 100000 个整数,但实际上可能是 1~100000 之间的任何数字!原因是 `accept()` 和 `get()` 并发执行,如果消费者速度慢,那 `accept()` 就会覆盖 `MutableRepository` 中的值。 192 | 193 | 有些情况下,这是可以接收的(就像应用了 RxJava 的 `onBackpressureDrop` 运算符一样),但有些情况下时不可接受的,因此我们可能会花费大量的时间排查为何数据会丢失。 194 | 195 | ### 错误处理 196 | 197 | 异步化通常意味着错误也得异步处理。RxJava 以及其他的响应式编程库在这一点上处理得很好:一旦某个环节发生了错误,整个处理流程就自动清理干净了,除非程序员希望错误发生后忽略错误、替代为默认值或者重试。错误处理和清理机制有时非常复杂,但库的开发者为此付出了巨大的努力,因此大家大多数情况下都不需要考虑这个问题。 198 | 199 | Agera 的基本 API 并没有考虑到错误处理,我们必须自己处理错误,就像处理数据一样。如果我们有多个 Agera 服务组合在一起,那我们必须实现一套在 callback-hell 情况下需要实现的错误处理机制。由于并发和终止状态的考虑,实现这样的错误处理机制是非常麻烦而且容易出错的。 200 | 201 | ### 终止状态 202 | 203 | Agera 没有对已终止数据流的表述方式:我们必须自行确定终止时间。这在 GUI 系统中常常没有问题,因为用户始终都在和界面打交道,不会终止。但是后台的异步任务就必须通过某种方式通知自己会发出多少事件,以及从何时开始 `update()` 将不会被调用了。 204 | 205 | ## 如何设计一套现代的无参数响应式 API 206 | 207 | 首先,我们不应该考虑自行设计,而是利用已有的接口设计: 208 | 209 | ~~~ java 210 | rx.Observable signaller = ... 211 | 212 | rx.Observer consumer = ... 213 | 214 | Subscription s = signaller.subscribe(consumer); 215 | 216 | // ... 217 | 218 | s.unsubscribe(); 219 | ~~~ 220 | 221 | 我们立即就拥有了所有的基础设施,操作符,以及高性能,几乎不需要付出任何代价。而且如果我们希望处理有意义的数据更新,那我们可以把 `Void` 替换为相应的数据类型。 222 | 223 | 如果现有的库看起来太重了,拥有太多用不到的操作符,那我们可以 fork 一份,删掉用不着的内容,然后利用之。当然,接下来我们需要保持更新,以应用 bugfix 和性能优化。 224 | 225 | 如果 fork 和删代码看起来不吸引人,那我们完全可以在 Reactive-Streams 规范的基础上实现自己的响应式编程库:`Publisher` 和 `Subscriber`,以及其他我们需要的代码。我们立刻就免费获得了和其他基于 Reactive-Streams 规范的响应式编程库交互的能力,此外,我们还能利用兼容性测试套件(TCK)来测试我们的实现。 226 | 227 | 当然,编写响应式编程库很难,基于 Reactive-Streams 规范实现更难。因此,最终的故事很可能是我们会从头实现一套准系统的 API。 228 | 229 | 如果大家确实希望实现一套自己的无参数响应式数据流,下面几点建议可以考虑一下: 230 | 231 | **1)不要有单独的 `addListener` 和 `removeListener` 接口**。单一的接口将会简化中间操作符的开发: 232 | 233 | ~~~ java 234 | interface Observable { 235 | Removable register(Updatable u); 236 | } 237 | 238 | interface Removable { 239 | void remove(); 240 | } 241 | ~~~ 242 | 243 | **2)考虑把取消/移除的支持注入到目标位置,而不是返回一个对象**: 244 | 245 | ~~~ java 246 | interface Observable { 247 | void register(Updatable u); 248 | } 249 | 250 | interface Updatable { 251 | void onRegister(Removable remover); 252 | void update(); 253 | } 254 | 255 | // or 256 | 257 | interface Updatable { 258 | void update(Removable remover); 259 | } 260 | ~~~ 261 | 262 | **3)至少考虑一下错误的传递**: 263 | 264 | 当然,这增加了库开发者的工作量,但能极大地简化库使用者的工作。 265 | 266 | ~~~ java 267 | interface Updatable { 268 | void onRegister(Removable remover); 269 | void update(); 270 | void error(Throwable ex); 271 | } 272 | ~~~ 273 | 274 | **4)提供可选的异步支持**。 275 | 276 | 在 `MutableRepository` 的例子中,我们可能希望在回到主线程之前,在调用者线程上线处理一下数据。这就意味着需要 [observeOn](/AdvancedRxJava/2016/09/16/subscribeon-and-observeon/index.html) 或者 `subscribeOn` 了(如果我们需要处理冷数据源的话)。 277 | 278 | ## 总结 279 | 280 | 编写一个响应式编程库并非易事,如果我们不熟悉这个领域的历史和演进,那我们可能会陷进很多坑里面。在很多公司里面,“非此制造”和“我们能做得更好”等理念如此强烈,以至于他们宁愿自己从头开始写,也不愿意基于他人已有的工作。 281 | 282 | (_有趣的是,当我向很多内部项目推荐 RxJava 时,对方仍表示很惊讶(getting raised eyebrows),即便他们已经快要自己编出一套 RxJava 了。_) 283 | 284 | 大家可能会问,我为啥要关心 Google/Agera?我对 RxJava 不自信吗?我当然很自信,而且 Agera 的出现对 RxJava 半毛钱影响都没有! 285 | 286 | 然而,根据[我以往的经验](/AdvancedRxJava/2017/03/25/asynchronous-event-streams-vs-reactive/index.html),如果有人空有噱头,口说无凭,盲目自信,那整个社区都可能会被带坏。我并不想对这件事做出太多评价,但想象一下,如果安卓的下一个版本强制以当前这个形态的 Agera 作为**标准的**安卓异步编程库(_那将多么糟糕..._)。 287 | 288 | (此外,[互操作](https://github.com/akarnokd/RxAgera)有时是不可避免的,我并不希望如果它们无法很好地互操作时,在 RxJava 的 issue 里面看到大家的抱怨。) 289 | 290 | 让我以一句慧语来作为结尾(目前有两个例证了):**你想要编写自己的响应式编程库吗?请别这样做!** 291 | --------------------------------------------------------------------------------