Reactor响应式编程系列(五)- 解读publishOn()方法
前言
Flux
和Mono
不会创建线程,只有当触发subscribe()
操作时才会执行对应的方法。而有些操作符,例如publishOn()
和subscribeOn()
方法,能够创建线程,本篇文章就针对publishOn()
来做个讲解,subscribeOn()
则放在下一篇博客中。
publishOn()
特性:
publishOn()
操作会强制下一个操作符(或许是下一个的下一个…)运行于不同的线程上。publishOn()
和其他操作符一样也处于整个订阅操作链中,从上游源获取元素,从关联的Scheduler
获取一个worker,并调用对应的schedule()
方法向下游发放元素。
以下内容指定reactor-core版本3.4.4
一. Mono.publishOn()源码解读
public final Mono<T> publishOn(Scheduler scheduler) {
// ...
return onAssembly(new MonoPublishOn<>(this, scheduler));
}
关注代码中的最后一行,可见new出了一个MonoPublishOn
对象,MonoPublishOn
继承了MonoOperator
类,来看下其继承关系:
可以见到最上层的Publisher
,说明了什么?说明MonoPublishOn
就是一个发布者,故看其subscribe()
方法:
final class MonoPublishOn<T> extends MonoOperator<T, T> {
public void subscribe(CoreSubscriber<? super T> actual) {
// source:该发布者的上一级
// PublishOnSubscriber类:用于维护订阅者和发布者的类
this.source.subscribe(new MonoPublishOn.PublishOnSubscriber(actual, this.scheduler));
}
}
以此来看下MonoPublishOn
的静态内部类PublishOnSubscriber
的继承关系:
可以发现,它实现了Runnable
接口,用于异步运行某个任务。PublishOnSubscriber
拥有者订阅和发布两个角色,本文关注其订阅的层面,因此来看下其onNext()
方法:
static final class PublishOnSubscriber<T> implements InnerOperator<T, T>, Runnable {
// 1.传参,执行
public void onNext(T t) {
// 接受上游数据后,进行赋值,然后调用trySchedule方法
this.value = t;
this.trySchedule(this, (Throwable)null, t);
}
// 2.交给调度器调度任务
void trySchedule(@Nullable Subscription subscription, @Nullable Throwable suppressed, @Nullable Object dataSignal) {
if (this.future == null) {
try {
// 该方法主要就是运行调度器,将自身作为任务,因为本身实现了Runnable接口。
this.future = this.scheduler.schedule(this);
} catch (RejectedExecutionException var5) {
this.actual.onError(Operators.onRejectedExecution(var5, subscription, suppressed, dataSignal, this.actual.currentContext()));
}
}
}
// 3.既然将自身当做任务直接交给调度器来调用,那么我们需要关注一下它的run()方法,(重写Runnable接口的run)
// 也就是启动任务时,需要执行的逻辑
public void run() {
if (!OperatorDisposables.isDisposed(this.future)) {
T v = VALUE.getAndSet(this, (Object)null);
// 如果当前的值不是空,那么就通过链式的方式来调用下级的onNext()方法。
// 也可以这么理解,即A-B-C-D-E-F-G,主线程Main在B处调用了publishOn方法,然后接下来的CDEFG则在另外一个线程thread上执行
if (v != null) {
this.actual.onNext(v);
this.actual.onComplete();
} else {
Throwable e = this.error;
if (e != null) {
this.actual.onError(e);
} else {
this.actual.onComplete();
}
}
}
}
}
1.1 案例
@Test
public void publishOnTest() {
Flux.range(1, 2)
.map(i -> {
System.out.println(Thread.currentThread().getName());
return i;
})
.publishOn(Schedulers.single())
.map(i -> {
System.out.println(Thread.currentThread().getName());
return i;
})
.publishOn(Schedulers.newParallel("parallel", 4))
.map(i -> {
System.out.println(Thread.currentThread().getName());
return i;
})
.subscribe();
}
结果如下:可以发现线程确确实实都发生了改变。
1.2 总结
调用publishOn()
时的过程:
- 最终方法return的时候,传入了一个
MonoPublishOn
类型的对象,其顶层父类包含Publisher
,因此可以看做返回了一个新的发布者对象。 MonoPublishOn
中的subscribe()
方法,从当前发布者的上一级处起
,开始产生新的订阅关系,传入PublishOnSubscriber
类型的对象。PublishOnSubscriber
用于维护订阅者和发布者,最终实现了Runnable
接口,因此当前实例可以看做为一个task,用于异步的运行当前任务。- 会调用
onNext()
方法,接受上游数据,并将当前task交给调度器去调度,调度时通过链式的方式来调用下级的onNext()
方法。
我们知道,序列在subscribe之前不会发生任何事情。那么publishOn()
这方法的本质,也就是新建了一个线程任务PublishOnSubscriber
,同时拥有订阅方法并执行调用(订阅动作能让动作链执行),也就相当于publishOn()
后面的逻辑都执行在另外一个线程上了。
二. Flux.publishOn()源码解读
以同样的方式进入publishOn()
方法:
// 一般我们在开发过程中,都只传入一个Scheduler类型的参数,但是从源码角度来看可以发现
// 我们无须指定队列的大小,用系统默认即可(256)
public final Flux<T> publishOn(Scheduler scheduler) {
return this.publishOn(scheduler, Queues.SMALL_BUFFER_SIZE);
}
↓↓↓↓↓
public final Flux<T> publishOn(Scheduler scheduler, int prefetch) {
return this.publishOn(scheduler, true, prefetch);
}
↓↓↓↓↓
public final Flux<T> publishOn(Scheduler scheduler, boolean delayError, int prefetch) {
return publishOn(scheduler, delayError, prefetch, prefetch);
}
↓↓↓↓↓
final Flux<T> publishOn(Scheduler scheduler, boolean delayError, int prefetch, int lowTide) {
// ...
// Queues.get(prefetch)也就是返回一个有界队列SpscArrayQueue
return onAssembly(new FluxPublishOn<>(this, scheduler, delayError, prefetch, lowTide, Queues.get(prefetch)));
}
2.查看对应的FluxPublishOn
,看下它的继承关系:
同样,由于其最终父类包含Publisher
接口,因此查看该类中有关订阅的方法subscribeOrReturn()
:
final class FluxPublishOn<T> extends InternalFluxOperator<T, T> implements Fuseable {
final Scheduler scheduler;
final boolean delayError;
final Supplier<? extends Queue<T>> queueSupplier;
final int prefetch;
final int lowTide;
@Override
@SuppressWarnings("unchecked")
public CoreSubscriber<? super T> subscribeOrReturn(CoreSubscriber<? super T> actual) {
// 1.获取一个worker实例
Worker worker = Objects.requireNonNull(scheduler.createWorker(),
"The scheduler returned a null worker");
// ..
// 2.将各种参数包装成一个PublisbOnSubscriber
return new PublishOnSubscriber<>(actual,
scheduler,
worker,
delayError,
prefetch,
lowTide,
queueSupplier);
}
}
其实到这里,跟Mono.publishOn()
方法的流程都是非常相似的,都是:
- 调用
xxx.publishOn()
方法都需要生成xxxPublishOn
对象。 xxxPublishOn
对象都实现了Publisher
接口,都有相关的subscribe()
方法。- 相关
subscribe()
方法最后都会生成一个xxxPublishOn.PublishOnSubscriber
对象。
对于前缀为Mono
的分支,我对于其内部的PublishOnSubscriber
类只是概括成一个用于维护订阅和发布的一个类,并没有详细解释,那么在Flux
这一分支就好好讲一讲。
2.1 PublisbOnSubscriber的解读
备注:下面的类位于FluxPublishOn
类中,记得区分MonoPublishOn
。
final class FluxPublishOn<T> extends InternalFluxOperator<T, T> implements Fuseable {
static final class PublishOnSubscriber<T>
implements QueueSubscription<T>, Runnable, InnerOperator<T, T> {
可见,FluxPublishOn.PublishOnSubscriber
比MonoPublishOn.PublishOnSubscriber
多实现了一个接口:QueueSubscription
。为什么会有这样的情况,接下来开始讲解。
当我们生产源无须存储元素,并且根据状态来进行元素下发的时候,也就是状态感知型下发操作。
以Flux.generate
为例,我们看下FluxGenerate.subscribe()
方法调用的时候会发生什么:
@Override
public void subscribe(CoreSubscriber<? super T> actual) {
S state;
try {
state = stateSupplier.call();
} catch (Throwable e) {
Operators.error(actual, Operators.onOperatorError(e, actual.currentContext()));
return;
}
// 该方法接收了一个FluxGenerate.GenerateSubscription对象
// 当下游操作链中需要执行异步化切换钱程的操作时,就将 publisbOn 放在 Flux.generale 之后
// 此时会调用FluxPublishOn.PublishOnSubscriber中的onSubscribe方法
actual.onSubscribe(new GenerateSubscription<>(actual, state, generator, stateConsumer));
}
先来看下FluxPublishOn
实现的接口Fuseable
:可以发现这里定义了几个字段。
public interface Fuseable {
/** Indicates the QueueSubscription can't support the requested mode. */
int NONE = 0;
/** Indicates the QueueSubscription can perform sync-fusion. */
int SYNC = 1;
/** Indicates the QueueSubscription can perform only async-fusion. */
int ASYNC = 2;
/** Indicates the QueueSubscription should decide what fusion it performs (input only). */
int ANY = 3;
/**
* Indicates that the queue will be drained from another thread
* thus any queue-exit computation may be invalid at that point.
*/
int THREAD_BARRIER = 4;
紧接着再回到PublishOnSubscriber
类,看下其onSubscribe
方法:
@Override
public void onSubscribe(Subscription s) {
if (Operators.validate(this.s, s)) {
this.s = s;
if (s instanceof QueueSubscription) {
@SuppressWarnings("unchecked") QueueSubscription<T> f =
(QueueSubscription<T>) s;
// 1.到这里对应着两种不同的方法,看下面 注释A 和 注释B 两种代码
// Fuseable.ANY=3,Fuseable.THREAD_BARRIER=4, 3 | 4 = 7
// 这里调用的方法是下面代码B的,而最终返回的值是None,也就是0
int m = f.requestFusion(Fuseable.ANY | Fuseable.THREAD_BARRIER);
// 因此这里的代码分支都不会走到,都跳过
if (m == Fuseable.SYNC) {
// ..
}
if (m == Fuseable.ASYNC) {
sourceMode = Fuseable.ASYNC;
queue = f;
actual.onSubscribe(this);
s.request(Operators.unboundedOrPrefetch(prefetch));
return;
}
}
// 2.得到有界队列SpscArrayQueue,publisbOn中的异步操作主要是该队列来实现的
// 通过这个 queue将上游源与下游订阅者分隔开来.
queue = queueSupplier.get();
// 3.由下游的传入的订阅者执行下面的方法。
actual.onSubscribe(this);
s.request(Operators.unboundedOrPrefetch(prefetch));
}
}
// A:reactor.core.publisher.FluxPublishOn.PublishOnSubscriber.requestFusion
@Override
public int requestFusion(int requestedMode) {
if ((requestedMode & ASYNC) != 0) {
outputFused = true;
return ASYNC;
}
return NONE;
}
// B:reactor.core.publisher.FluxGenerate.GenerateSubscription.requestFusion
@Override
public int requestFusion(int requestedMode) {
if ((requestedMode & Fuseable.SYNC) != 0 && (requestedMode & Fuseable.THREAD_BARRIER) == 0) {
outputFused = true;
return Fuseable.SYNC;
}
return Fuseable.NONE;
}
言归正传,在Flux.generate
生产源的场景下,public void onSubscribe(Subscription s)
中传入的参数类型是GenerateSubscription
,它实现了QueueSubscription
接口,即f.requestFusion
方法走的是上面注释B中的逻辑,
因此传入的参数为7,在代码B中的If块
中的值是False
。因此最终返回Fuseable.NONE
,对应的值是0。因此下面两个If块
直接跳过,可以获得一个有界队列,而我们的异步就是靠其来实现的。
问题:
因为下发和请求操作依然是在异步线程里执行的,所以它们可能会分布在多个线程中执行任务。如何保证请求的单一性?
解决:
- 将上游元素下发到这个
queue
中,当下游多个线程发送请求的时候,直接增加PublishOnSubscriber
中定义的REQUESTED
数量 - 接着等待
PublishOnSubscriber
从这个queue
中获取元素并下发给下游订阅者。
同时,Flux.generate
产生源并调用publishOn()
方法,会调用到下面的request
方法:
// FluxGenerate.GenerateSubscription
@Override
public void request(long n) {
if (Operators.validate(n)) {
if (Operators.addCap(REQUESTED, this, n) == 0) {
if (n == Long.MAX_VALUE) {
fastPath();
} else {
slowPath(n);
}
}
}
}
我们知道request()
也就是一个线程一次的请求数据的数量,从代码来看,无非就是执行两种情况:
- fastPath
- slowPath
但是在多线程的情况下,毫无疑问的会多次调用,并且在n没有达到最大值的情况下,代码一般会调用slowPath(n)
:
// 也就是会多次调用apply方法
void slowPath(long n) {
S s = state;
// ...
try {
s = g.apply(s, this);
if (!hasValue) {
cleanup(s);
actual.onError(new IllegalStateException("The generator didn't call any of the " +
"SynchronousSink method"));
return;
}
// 若正常执行,这个hasValue就会赋值为false
hasValue = false;
// ..
}
什么意思呢?就是一个线程执行完后,可能将hasValue
改为False
,那么另外的线程在执行相同的代码的时候,遇到if (!hasValue)->true
代码,就直接抛异常了。那么该如何避免呢?
2.1.1 多次调用publishOn的情况下避免异常
前面分析到,以Flux.generate
为例,第一次调用publishOn()
的情况下,会走注释B代码,得到结果None
,但是在第二次调用publishOn()
的情况下,会执行到PublishOnSubscriber.requestFusion()
方法(注释A),会得到ASYNC
(赋值给sourceMode
属性),并将上一个PublishOnSubscriber
的outputFused
属性改为true。并且,在调用onNext()
方法的时候:(代码流程讲的是第二次调用publishOn的情况)
// PublishOnSubscriber.PublishOnSubscriber
@Override
public void onNext(T t) {
// 此时sourceMode = ASYNC ,代码走该分支
// 此时不会下发任何元素,这是因为元素都由它前面的那个publishOn操作放置在它自己的queue中。
// 后面这个publishOn操作只需要在trySchedule中从队列中拉取元素井下发即可。
if (sourceMode == ASYNC) {
trySchedule(this, null, null /* t always null */);
return;
}
if (done) {
Operators.onNextDropped(t, actual.currentContext());
return;
}
if (cancelled) {
Operators.onDiscard(t, actual.currentContext());
return;
}
if (!queue.offer(t)) {
Operators.onDiscard(t, actual.currentContext());
error = Operators.onOperatorError(s,
Exceptions.failWithOverflow(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL),
t, actual.currentContext());
done = true;
}
// 第一次调用publishOn走的流程
trySchedule(this, null, t);
}
//
void trySchedule(
@Nullable Subscription subscription,
@Nullable Throwable suppressed,
@Nullable Object dataSignal) {
if (WIP.getAndIncrement(this) != 0) {
// ..
try {
// 因为PublishOnSubscriber实现了Runnable接口,因此本身就是个线程任务,则直接传入this,执行调度
worker.schedule(this);
}
// ..
}
// 再看下其run方法的实现:
@Override
public void run() {
// 第二次执行publishOn时(一条链),此时outputFused为true,执行该分支代码,什么意思呢?
// 当一个调用链中出现多次publisbO 操作的时候,后面的那一个操作会通过QueueSubscriprion废弃前一个publishOo操作。
// 也就是说上一个publishOn只会切换线程,而并不会做与真实下发元素相关的其他任何事情
if (outputFused) {
runBackfused();
}
else if (sourceMode == Fuseable.SYNC) {
runSync();
}
else {
// 第一次publishOn会执行该代码
runAsync();
}
}
可以发现如果调用两次publishOn()
,会将上一个publishOn()
操作给废弃掉,即保证了不会报错,那么第一次publishOn()
的时候发生了什么?最后会调用到runAsync()
方法:
void runAsync() {
int missed = 1;
final Subscriber<? super T> a = actual;
// 1.这里的queue就是上文提到的SpscArrayQueue类,其内部是由原子类来保证操作的安全性
// 既可以保证多线程下的并发获取操作
final Queue<T> q = queue;
long e = produced;
for (; ; ) {
long r = requested;
while (e != r) {
boolean d = done;
T v;
try {
v = q.poll();
}
// ..
// 当元素生产速度小于消费速度时,上述q.poll得到的值为null
// 若empty为true则跳出循环
// 当元素生产速度大于消费速度时,工作的钱程会不断地从FluxPublishOn的暂存队列中获取元素进行消费
// 此时不会出现消费线程反复切换的情况
boolean empty = v == null;
e++;
// 即消费元素个数 = publishOn操作请求上游元素个数
if (e == limit) {
if (r != Long.MAX_VALUE) {
// 此时会通过原子类操作REQUESTED来保证元素请求数量的原子性
// 此时队列中已经没有元素了,因此需要publishOn操作去上游拉取元素,
// 首先,将下游订阅者所需总元素数量减去这次要请求元素的数量:r-e
r = REQUESTED.addAndGet(this, -e);
}
// 在执行该操作,e代表一次能请求的元素个数最大值
s.request(e);
// 请求完毕,将e设定为0
e = 0L;
}
}
if (e == r && checkTerminated(done, q.isEmpty(), a, null)) {
return;
}
int w = wip;
if (missed == w) {
produced = e;
missed = WIP.addAndGet(this, -missed);
if (missed == 0) {
break;
}
}
else {
missed = w;
}
}
}
2.1.2 案例
源中的元素生成速度<消费速度(代码中sleep了100ms)
@org.junit.Test
public void test5() throws Exception {
final Random random = new Random();
Flux.generate(ArrayList::new, (list, sink) -> {
int value = random.nextInt(100);
list.add(value);
System.out.println("线程" + Thread.currentThread().getName() + "发射元素");
sink.next(value);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 0) {
sink.complete();
}
return list;
}).publishOn(Schedulers.parallel(), 2)
.map(x -> String.format("[%s} %s", Thread.currentThread().getName(), x))
.subscribe(System.out::println);
Thread.sleep(30000);
}
结果如下:
若去掉睡眠的时间,结果如下(我试了好多遍才有这样的结果,建议大家点击Run后狂点停止按钮,否则输出结果太多,最前面的内容会被屏蔽):
两者有什么区别?
- 结果1:都是主线程Main在输出。parallel-1负责消费。即产生了线程不断切换的情况。
- 结果2:主线程Main输出2条数据后,由parallel-1线程来输出。parallel-1负责消费数据,即没有结果1那样,线程在不断地切换。
这佐证了上面注释中提到的:
- 当元素生产速度小于消费速度时(结果1),如果我们所用调度器底层执行者为多个线程的线程池,那么就会看到线程切换的情况 。(主线程输出,parallel-1)
- 当元素生产速度大于消费速度时(结果2),工作的线程会不断地从FluxPublishOn的暂存队列中获取元素进行消费,这样就不会出现消费线程反复切换的情况。
2.2 总结
Flux.publishOn()
的源码讲解我主要参考的是《响应式Spring Reactor 3设计与实现》这本书,我自己来回看了好几遍,最后才懂,真的难搞哦。我自己写的也可能有点乱(真的抱歉),跟着书的节奏来写的,所以我在这里先进行个总结,方便大家理解。
总结:
Flux.publishOn()
方法最后需要传入一个FluxPublishOn
类型的对象,其顶层父类包含Publisher
,因此可以看做返回了一个新的发布者对象。- 查看其
subscribeOrReturn()
订阅方法,最后将各种参数包装成新对象PublishOnSubscriber
返回(类同Mono
)。 FluxPublishOn.PublishOnSubscriber
比MonoPublishOn.PublishOnSubscriber
多实现了一个接口:QueueSubscription
,目的是为了多线程异步的任务处理,保证高并发下程序运行正常。- 后面给的分析是基于
Flux.generate
为例,并且调用链调用了两次publishOn()
的情况 ,当调用对应的publishOn
时,都会调用到PublishOnSubscriber
中的onSubscribe()
方法,该方法做了这么几个事情。
第一次调用时:产生的PublishOnSubscriber记为P1
-----1.调用FluxGenerate.GenerateSubscription.requestFusion
方法,返回NONE
-----2.得到有界队列SpscArrayQueue
中,记为A
,将上游的源和下游的订阅分隔开来,即将上游的元素都放入到队列A中,由下游的传入的订阅者执行后续的方法。
-----3.在调用publishOn操作相关的onNext方法时,由于sourceMode=NONE
,调用方法trySchedule(this, null, t)
。
-----4.由于P1实现了Runnable
接口,最后实现其run()
方法,执行runAsync()
逻辑。
第二次调用时:产生的PublishOnSubscriber记为P2
-----1.调用FluxGenerate.PublishOnSubscriber.requestFusion
方法,返回ASYNC
-----2.并将上一个PublishOnSubscriber
的outputFused
属性设置为True
。
-----3.在调用publishOn
操作相关的onNext
方法时,由于sourceMode=ASYNC
,调用方法trySchedule(this, null, null)
,该方法不会下发任何元素,而只需要从队列A
中拉取元素即可。
-----4.由于P2实现了Runnable
接口,最后实现其run()
方法,但是由于outputFused=true
,执行runBackfused()
逻辑,通过QueueSubscriprion
废弃前一个publishOn
操作。
通俗点来说就是(个人理解,若有出入或者错,还望指正!):
- 第一次publishOn操作记为A,第二次publishOn操作记为B。第三次publishOn操作记为C。数据总数量为M。
- A负责另起一个线程,创建一个队列,并把元素扔进去。而操作则交给B来处理。
- B负责另起一个线程,去队列中拉取元素去消费,同时废弃操作A,然后自身处理一次请求,处理掉对应数量的数据
request(n)
,总数据量-n。 - C负责另起一个线程,去队列中拉取元素去消费,同时废弃操作B,然后自身处理一次请求,处理掉对应数量的数据
request(n)
,总数据量-n,后续以此类推
并且根据案例我们需要注意两点:
- 元素生产速度小于消费速度时,在
FluxPublishOn
中出现的情况是暂存元素队列调用poll方法得到的是null,即空队列,会看到线程切换的情况。 - 元素生产速度大于消费速度时,工作的钱程会不断地从
FluxPublishOn
的暂存队列中获取元素进行消费 , 这样就不会出现消费线程反复切换的情况。
本篇文章的精髓就是:
就是后者通过一个接口
QueueSubscriprion
,一个本地状态sourceMode
,一个链式的调用方法PublishOnSubscriber.onSubscribe()
来根据前者的结果来进行自己的行为设定。
最后感谢大家能够耐心看到这里,下一篇文章会努力写好,下一篇文章就是分学习Reactor中的subscribeOn()
方法,并且在后续准备好好学习下QueueSubscriprion
类,看看他到底在Reactor框架中气到了什么作用。如果你喜欢这篇文章,还望给个三连~😝😝