Reactor响应式编程系列(六)- 解读subscribeOn()方法
前言
在上一篇文章中提到过,publishOn()
切换的是元素消费操作执行时所在的线程,其异步指的是元素的存储(放到队列Queue
中)和获取操作。而在本章节,主要讲subscribeOn()
操作,其主要针对的是发生订阅的线程。
一.深入解读subscribeOn
1.1 案例
假设我们以Flux.create()
作为源,并在生产元素的过程中执行一些阻塞操作,造成:元素的消费速度>生产速度
。案例如下:
@Test
public void fluxTest() throws InterruptedException {
final Random random = new Random();
Flux.create(sink -> {
ArrayList<Integer> list = new ArrayList<>();
Integer i = 0;
while (list.size() != 10) {
int value = random.nextInt(100);
list.add(value);
i += 1;
System.out.println(Thread.currentThread().getName() + "发射了元素" + i);
sink.next(value);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sink.complete();
}).doOnRequest(x -> System.out.println("..." + Thread.currentThread().getName()))
.subscribeOn(Schedulers.elastic())
.publishOn(MyScheduler(), 4)
.map(x -> String.format("[%s] %s", Thread.currentThread().getName(), "消费了元素"))
.subscribe(System.out::println);
Thread.sleep(10000);
}
public static Scheduler MyScheduler() {
Executor executor = new ThreadPoolExecutor(
10, //corePoolSize
10, //maximumPoolSize
0L, TimeUnit.MILLISECONDS, //keepAliveTime, unit
new LinkedBlockingQueue<>(1000), //workQueue
Executors.defaultThreadFactory()
);
return Schedulers.fromExecutor(executor);
}
运行结果如下:可以见到请求因为元素生产的速度较慢而进入阻塞,从而导致消费线程阻塞(代码中publishOn
中的prefetch
参数为4)。
1.2 源码分析
接下来开始进行解析,从subscribeOn()
方法开始:
// 可见调用了subscribeOn的重载方法,并且第二个参数默认是true
public final Flux<T> subscribeOn(Scheduler scheduler) {
return subscribeOn(scheduler, true);
}
↓↓↓↓↓
public final Flux<T> subscribeOn(Scheduler scheduler, boolean requestOnSeparateThread) {
// ..
return onAssembly(new FluxSubscribeOn<>(this, scheduler, requestOnSeparateThread));
}
类似于publishOn()
方法的解析,我们看其中传入了FluxSubscribeOn
这个类:
final class FluxSubscribeOn<T> extends InternalFluxOperator<T, T> {
final Scheduler scheduler;
final boolean requestOnSeparateThread;
FluxSubscribeOn(Flux<? extends T> source, Scheduler scheduler, boolean requestOnSeparateThread) {
super(source);
this.scheduler = Objects.requireNonNull(scheduler, "scheduler");
this.requestOnSeparateThread = requestOnSeparateThread;
}
@Override
public CoreSubscriber<? super T> subscribeOrReturn(CoreSubscriber<? super T> actual) {
Worker worker = Objects.requireNonNull(scheduler.createWorker(),
"The scheduler returned a null Function");
SubscribeOnSubscriber<T> parent = new SubscribeOnSubscriber<>(source,
actual, worker, requestOnSeparateThread);
actual.onSubscribe(parent);
try {
worker.schedule(parent);
}
// ..
}
很显然,FluxSubscribeOn
中的subscribeOrReturn()
方法(低版本的都是subscribe()
)主要就是用worker进行调度。但是在这之前,可以发现先调用了这行代码:actual.onSubscribe(parent);
,即下游调用上游Subscription
的request
方法(最终调用到该方法),同时我们能发现parent
是SubscribeOnSubscriber
的实例,因此我们来看下SubscribeOnSubscriber
的request()
方法:
@Override
public void request(long n) {
if (Operators.validate(n)) {
// 第一次的时候,get到的对象s为空。因为此时还并没有和上游的FluxCreate产生交互。
// 因此也拿不到对应的Subscription,所以第一次时request操作不会做任何事情。
Subscription s = S.get(this);
if (s != null) {
requestUpstream(n, s);
}
else {
Operators.addCap(REQUESTED, this, n);
s = S.get(this);
if (s != null) {
long r = REQUESTED.getAndSet(this, 0L);
if (r != 0L) {
requestUpstream(r, s);
// ..
此时紧接着执行worker.schedule(parent);
方法,也就会执行SubscribeOnSubscriber
的run()
方法,让上游和下游产生订阅关系。
@Override
public void run() {
THREAD.lazySet(this, Thread.currentThread());
source.subscribe(this);
}
来看下SubscribeOnSubscriber
类中的相关方法(FluxSubscribeOn
的内部类):
static final class SubscribeOnSubscriber<T> implements InnerOperator<T, T>, Runnable {
@Override
public void onSubscribe(Subscription s) {
if (Operators.setOnce(S, this, s)) {
long r = REQUESTED.getAndSet(this, 0L);
if (r != 0L) {
requestUpstream(r, s);
}
}
}
开始分析:
- 我们知道调用
subscribeOn()
方法时,第二个参数requestOnSeparateThread
默认是true
。 - 也就是说当下游没有
publishOn
这种切换线程的操作时,并且恰好产生了拉取元素的请求时,生产元素的过程中会有阻塞。(生产元素速度<消费速度) - 若下游有
publishOn
这种切换线程的操作时,对应两种情况:
1.在产生订阅并且发起拉取元素请求的时候,该请求和元素生产任务处于同一个线程,调用
onSubscribe()
方法
2.当publishOn操作需要请求时,该请求发生在元素消费任务所在的线程中,调用上述的requestUpstream(n, s);
方法(request中的方法)
即(将黑色的字体连起来读~):
- 当下游元素的消费速度>上游元素的生产速度时。会产生请求,调用
SubscribeOnSubscriber
的requestUpstream
方法。 - 其中内部会调用
worker.schedule(()->s.request(n))
,此时会将请求加入到线程A所在线程池中的任务队列中。(该线程池是单线程池) - 此时只有将上游订阅方法的任务逻辑完成之后,才能够执行后续的请求任务。
- 但是线程B(消费线程)由于request操作,处于等待状态而一直阻塞。
注意:
- 对于1.1的案例,只有在彻底生产完元素之后,请求才会继续执行,消费接下来的元素。
elastic()
针对的并发是多个发布-订阅之间的并发操作,而不是单个订阅关系内的并发操作。(因为其内部是返回了一个单线程池,只有一个线程)
因此为了解决案例1.1中的阻塞问题,我们需要将请求任务执行线程和元素生产任务线程分开,通过指定subscribeOn()
方法中的第二个参数为false
即可,运行结果如下:
1.3 总结
我个人理解就是:
subscribeOn()
影响的是源的操作,默认的情况下,其第二个参数requestOnSeparateThread
的值为true
。- 也就是说,
subscribeOn()
后紧跟着publishOn()
方法,导致请求任务的执行线程和元素生产任务线程是分开的,对于代码而言,相当于请求的消费线程有10个,但是和元素的生产线程是独立开的,因此消费不到对应的元素,即阻塞。 - 而一旦
requestOnSeparateThread
值改为false
,就允许在元素消费任务线程中直接进行请求元素。
二. publishOn和subscribeOn的区别
publishOn
一般使用在订阅链的中间位置,并且从下游获取信号,影响调用位置起后续运算的执行位置。subscribeOn
一般用于构造向后传播的订阅过程。并且无论放到什么位置,它始终会影响源发射的上下文。同时不会影响对publishOn
的后续调用的行为。publishOn
会强制让下一个运算符(或者下下个)运行于不同的线程上,subscribeOn
会强制让上一个(或者上上个)运算符在不同的线程上执行。