Reactor响应式编程系列(六)- 解读subscribeOn()方法

Reactor响应式编程系列(六)- 解读subscribeOn()方法

Reactor响应式编程系列导航

前言

在上一篇文章中提到过,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);,即下游调用上游Subscriptionrequest方法(最终调用到该方法),同时我们能发现parentSubscribeOnSubscriber的实例,因此我们来看下SubscribeOnSubscriberrequest()方法:

@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);方法,也就会执行SubscribeOnSubscriberrun()方法,让上游和下游产生订阅关系。

@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中的方法)

即(将黑色的字体连起来读~):

  1. 当下游元素的消费速度>上游元素的生产速度时。会产生请求,调用SubscribeOnSubscriberrequestUpstream方法。
  2. 其中内部会调用worker.schedule(()->s.request(n)),此时会将请求加入到线程A所在线程池中的任务队列中。(该线程池是单线程池)
  3. 此时只有将上游订阅方法的任务逻辑完成之后,才能够执行后续的请求任务。
  4. 但是线程B(消费线程)由于request操作,处于等待状态而一直阻塞。

注意:

  • 对于1.1的案例,只有在彻底生产完元素之后,请求才会继续执行,消费接下来的元素。
  • elastic()针对的并发是多个发布-订阅之间的并发操作,而不是单个订阅关系内的并发操作。(因为其内部是返回了一个单线程池,只有一个线程)

因此为了解决案例1.1中的阻塞问题,我们需要将请求任务执行线程和元素生产任务线程分开,通过指定subscribeOn()方法中的第二个参数为false即可,运行结果如下:
在这里插入图片描述

1.3 总结

我个人理解就是:

  1. subscribeOn()影响的是源的操作,默认的情况下,其第二个参数requestOnSeparateThread的值为true
  2. 也就是说,subscribeOn()后紧跟着publishOn()方法,导致请求任务的执行线程和元素生产任务线程是分开的,对于代码而言,相当于请求的消费线程有10个,但是和元素的生产线程是独立开的,因此消费不到对应的元素,即阻塞。
  3. 而一旦requestOnSeparateThread值改为false,就允许在元素消费任务线程中直接进行请求元素。

二. publishOn和subscribeOn的区别

  • publishOn一般使用在订阅链的中间位置,并且从下游获取信号,影响调用位置起后续运算的执行位置。
  • subscribeOn一般用于构造向后传播的订阅过程。并且无论放到什么位置,它始终会影响源发射的上下文。同时不会影响对publishOn的后续调用的行为。
  • publishOn会强制让下一个运算符(或者下下个)运行于不同的线程上,subscribeOn会强制让上一个(或者上上个)运算符在不同的线程上执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值