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 .anchorjs-link, .anchorjs-link:focus { opacity: 1; }",n=' @font-face { font-family: "anchorjs-icons"; font-style: normal; font-weight: normal; src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBTUAAAC8AAAAYGNtYXAWi9QdAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5Zgq29TcAAAF4AAABNGhlYWQEZM3pAAACrAAAADZoaGVhBhUDxgAAAuQAAAAkaG10eASAADEAAAMIAAAAFGxvY2EAKACuAAADHAAAAAxtYXhwAAgAVwAAAygAAAAgbmFtZQ5yJ3cAAANIAAAB2nBvc3QAAwAAAAAFJAAAACAAAwJAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpywPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6cv//f//AAAAAAAg6cv//f//AAH/4xY5AAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAACADEARAJTAsAAKwBUAAABIiYnJjQ/AT4BMzIWFxYUDwEGIicmND8BNjQnLgEjIgYPAQYUFxYUBw4BIwciJicmND8BNjIXFhQPAQYUFx4BMzI2PwE2NCcmNDc2MhcWFA8BDgEjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAEAAAABAACiToc1Xw889QALBAAAAAAA0XnFFgAAAADRecUWAAAAAAJTAsAAAAAIAAIAAAAAAAAAAQAAA8D/wAAABAAAAAAAAlMAAQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAACAAAAAoAAMQAAAAAACgAUAB4AmgABAAAABQBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEADgAAAAEAAAAAAAIABwCfAAEAAAAAAAMADgBLAAEAAAAAAAQADgC0AAEAAAAAAAUACwAqAAEAAAAAAAYADgB1AAEAAAAAAAoAGgDeAAMAAQQJAAEAHAAOAAMAAQQJAAIADgCmAAMAAQQJAAMAHABZAAMAAQQJAAQAHADCAAMAAQQJAAUAFgA1AAMAAQQJAAYAHACDAAMAAQQJAAoANAD4YW5jaG9yanMtaWNvbnMAYQBuAGMAaABvAHIAagBzAC0AaQBjAG8AbgBzVmVyc2lvbiAxLjAAVgBlAHIAcwBpAG8AbgAgADEALgAwYW5jaG9yanMtaWNvbnMAYQBuAGMAaABvAHIAagBzAC0AaQBjAG8AbgBzYW5jaG9yanMtaWNvbnMAYQBuAGMAaABvAHIAagBzAC0AaQBjAG8AbgBzUmVndWxhcgBSAGUAZwB1AGwAYQByYW5jaG9yanMtaWNvbnMAYQBuAGMAaABvAHIAagBzAC0AaQBjAG8AbgBzRm9udCBnZW5lcmF0ZWQgYnkgSWNvTW9vbi4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format("truetype"); }',i=" [data-anchorjs-icon]::after { content: attr(data-anchorjs-icon); }";e.className="anchorjs",e.appendChild(document.createTextNode("")),A=document.head.querySelector('[rel="stylesheet"], style'),void 0===A?document.head.appendChild(e):document.head.insertBefore(e,A),e.sheet.insertRule(t,e.sheet.cssRules.length),e.sheet.insertRule(o,e.sheet.cssRules.length),e.sheet.insertRule(i,e.sheet.cssRules.length),e.sheet.insertRule(n,e.sheet.cssRules.length)}}}var anchors=new AnchorJS;
7 |
--------------------------------------------------------------------------------
/_posts/2016-10-06-operator-internals-s-amb-ambwith.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: post
3 | title: 深入理解 Operator:Amb 和 AmbWith
4 | tags:
5 | - Operator
6 | ---
7 |
8 | 原文 [Operator internals: Amb, AmbWith](http://akarnokd.blogspot.com/2015/10/operator-internals-amb-ambwith.html){:target="_blank"}
9 |
10 | ## 介绍
11 |
12 | `amb` 是 `ambiguous` 的缩写,它的输入是一个 `Observable` 集合,输出的是转发第一个发出事件的 Observable 的所有后续事件,并且取消订阅其他所有的 Observable,也会忽略它们的任何事件。amb 支持 backpressure。
13 |
14 | 从编写操作符的角度出发,我们需要考虑以下几个属性/要求:
15 |
16 | + 当下游订阅的时候,数据源 `Observable` 的数量是已知的。
17 | + 我们需要一个 `CompositeSubscription` 之外的容器类,因为它不支持从中挑出最先发事件的然后取消其他的。
18 | + 我们需要把下游的请求转发给所有的数据源。
19 | + 在我们订阅数据源的过程中,取消订阅、请求数据、甚至是“赢家”的出现,都随时可能发生。
20 |
21 | 两个大版本间的实现方式稍有不同。
22 |
23 | ## 1.x 的实现
24 |
25 | 1.x 的实现有些冗余。它利用 `ConcurrentLinkedQueue` 来保存 `AmbSubscriber` 列表,然后胜出的 AmbSubscriber 被保存在一个 `AtomicReference` 中。
26 |
27 | 为了处理取消订阅,我们就需要在 child 中注册一个取消订阅的回调,这样在取消订阅的时候,我们就可以逐个把数据源取消订阅了。
28 |
29 | 当下游订阅的时候,我们会遍历所有的数据源,为每个 Observable 创建一个 AmbSubscriber,然后订阅它。由于取消订阅或者胜出都可能在遍历过程中发生,所以我们在遍历过程中需要检查这两种情况,以便尽早退出循环。
30 |
31 | 当所有的数据源都订阅之后,我们就会为 child 设置一个 `Producer`。它的任务就是把 child 的请求转发给所有的 AmbSubscriber,或者有数据源胜出之后,只转发给胜利者。
32 |
33 | 这看起来有点奇怪,因为如果在设置 Producer 之前就有了胜利者,那我们也就没必要设置 Producer 了,因为这个胜利者并没有按照 backpressure 的要求行事(_译者注:都还没有设置 Producer,也就不会有请求,没有收到请求就发出了数据,这就是没有遵守 backpressure 的要求嘛_)。
34 |
35 | 在 AmbSubscriber 中,收到任何事件之后,它都会检查自己是不是胜利者,如果是,就把事件转发给 child。否则,它会尝试用 CAS 操作把自己设置为胜利者,如果成功了,它会取消订阅所有其他的 AmbSubscriber(_并且转发事件_),如果失败了,那就把自己取消订阅掉。
36 |
37 | ## 2.x 的实现
38 |
39 | 2.x 的实现会简洁一些,而且还会利用“被下游订阅时数据源的数量是已知的”这一事实。所以 `AmbInnerSubscriber` 将会用一个数组保存,而胜利者标志则用一个 `volatile int` 来记录,它的更新是通过一个“成员变量更新器(field updater)”实现的(_译者注:当我翻译到这篇文章时,一年时间过去了,现在 winner 已经换成了用 `AtomicInteger` 来实现。_)。
40 |
41 | 当 child 订阅时,我们会进行两次循环,第一次我们会为每个数据源创建一个 AmbInnerSubscriber,然后在 child 中加入一个自定义的 `Subscription` 对象(`AmbCoordinator`),第二次我们会逐个订阅数据源。第二次循环过程中会检查是否已经有了胜利者。
42 |
43 | `winner` 成员变量在不同的状态下有不同的含义。-1 表示 child 已经取消订阅了,0 表示当前没有胜利者,正数则表示胜利者的下标加一。
44 |
45 | 在响应式编程的世界里,`Subscription` 比数据请求、取消订阅来得晚的可能性在逐渐增加,所以我们必须考虑这种情况。因此 AmbInnerSubscriber 必须用一个 volatile 成员来保存它的 Subscription,而且还要用另一个成员记录积累的请求量。这种模式在 2.x 中用得非常多,所以我们在有一个专门的类负责这件事:
46 |
47 | ~~~ java
48 | class AmbSubscriber
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 extends Publisher extends T>> factory;
30 | public OnSubscribeDefer(
31 | Func0 extends Publisher extends T>> factory) {
32 | this.factory = factory;
33 | }
34 | @Override
35 | public void call(Subscriber super T> child) {
36 |
37 | SubscriptionArbiter sa = new SubscriptionArbiter();
38 | child.onSubscribe(sa); // (1)
39 |
40 | Publisher extends T> 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 super T> 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