Reactor3 Flux.create与Flux.push区别与源码分析(二)


系列文章

Reactor3 SpscLinkedArrayQueue源码分析🔥
Reactor3 MpscLinkedQueue源码分析🔥🔥
Reactor3 Flux.create与Flux.push正确打开方式🔥🔥🔥
Reactor3 Flux.create与Flux.push区别与源码分析(一)🔥🔥🔥🔥🔥

版本

Reactor 3.4.9

写在前面

为了介绍Flux的create与push的源码写了上面三篇博客做铺垫,不太容易👻。
如果不懂Flux.create与push使用请阅读:Reactor3 Flux.create与Flux.push正确打开方式👈
如果不懂SpscLinkedArrayQueue请点击:Reactor3 SpscLinkedArrayQueue源码分析👈
如果不懂MpscLinkedQueue请进入:Reactor3 MpscLinkedQueue源码分析👈


上一篇中我们分析了Flux.create与Flux.push外部的一些源码,内部的核心源码设计留在了本篇。
我们首先回顾一下reactor.core.publisher.FluxCreate#subscribe方法的步骤:

  • 先创建了一个BaseSinkpushcreate都是使用的默认的BufferAsyncSink。这里就不放代码了,里面逻辑不复杂。
  • BaseSink当做一个Subscription传给Subscriber,作为FluxCreatepushcreate都是使用的默认的BufferAsyncSink。与Subscriber的桥梁。
  • 接着就是黑魔法啦,通过CreateMode模式的不同,来对createsink使用SerializedFluxSink多进行了一层封装。

可以看到最先用到的是BaseSink然后是BufferAsyncSink与SerializedFluxSink,我们就根究这个顺序来逐个分析吧。

BaseSink源码分析

在上一篇中我们看到了BaseSink是实现了FluxSink与Subscription两个接口,充当FluxCreate与Subscriber直接的桥梁。
BaseSink它是一个抽象类,其他的Sink继承它。它实现了一些通用的方法比如requestcompleteerrorcancel等。而next这个下发元素的重要方法则有下面的子类根据自身的特性去实现。

BaseSink构造方法
		BaseSink(CoreSubscriber<? super T> actual) {
			this.actual = actual;
			this.ctx = actual.currentContext();
		}

常规操作,将订阅者保存到自己成员变量中,以便后面与订阅者通信。

BaseSink complete,error,cancel
	public void complete() {
			if (isTerminated()) {
				return;
			}
			try {
				actual.onComplete();
			}
			finally {
				disposeResource(false);
			}
		}
  • 判断是否已经被销毁
  • 没有被销毁则调用订阅者的onComplete方法,最后一定会调用销毁方法。

其他方法都大同小异,没有什么黑魔法,就不一一叙述了。

BufferAsyncSink源码分析

上面的BaseSink只是开胃菜,实现的都是一些通用的方法,而BufferAsyncSink看名称就能猜到它是有两个特性:缓冲与异步。它是Flux.push直接使用的队列,当然Flux.create也有使用到,不过被封装了一遍。在上一篇中,我们说过Flux.push只允许单线程的提交任务,它是怎么实现的?

BufferAsyncSink成员变量介绍
		//下发元素的缓冲队列
		final Queue<T> queue;
		//记录错误
		Throwable error;
		//判断是否结束
		volatile boolean done; 
		//可以把它理解成drain方法被调用了多少次,
		//只有当读取wip为0的线程才被允许去进行将queue
		//中的元素下发到订阅者的操作。
		volatile int wip;

BufferAsyncSink的Buffer与Async就呼之欲出了。它的成员变量queue就是用来做Buffer的,先将任务提交到queue,然后异步的下发的订阅者。

BufferAsyncSink 构造方法
		BufferAsyncSink(CoreSubscriber<? super T> actual, int capacityHint) {
			super(actual);
			this.queue = Queues.<T>unbounded(capacityHint).get();
		}

重点在队列的创建,最后返回的是一个SpscLinkedArrayQueue。看过Reactor3 SpscLinkedArrayQueue源码分析的同学都清楚这个队列的特性:单生产者-单消费者。如果不了解具体实现可以点击Reactor3 SpscLinkedArrayQueue源码分析👈

BufferAsyncSink request方法
		public FluxSink<T> next(T t) {
			queue.offer(t);
			drain();
			return this;
		}
  • 将元素直接放入队列中
  • 调用drain方法
    我们在分析SpscLinkedArrayQueue时,它虽然说是单生产者,但是它并没有自己控制整个特性,而是交给调用者自己控制。在BufferAsyncSink中它也没有控制只允许一个线程去给队列插入元素。所以,如果使用多个线程并发使用Flux.push去下发元素,会有并发安全问题。
Flux.push 多线程下发元素的例子
	public void testFluxPush() throws InterruptedException {
        //多线程调用push方法

        Flux<String> f = Flux.push(sink -> {
            outSink = sink;
        });
        f.subscribe(e -> System.out.println(e));
        //do something

        //下发元素
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> outSink.next("我来了" + finalI)).start();
        }
        Thread.sleep(1000);
    }

执行结果
在这里插入图片描述
我们可以看到丢失了几个任务。
我们在使用Flux.push的时候需要自己去控制只能有单个线程去调用Flux.push的元素下发。 读者可以将push换成create,看结果有何不同。

BufferAsyncSink drain方法

next方法中,下发完元素后都会调用一下这个方法。其实不仅是next,BufferAsyncSink 其他的所有操作都会调用drain

 		void drain() {
            //如果wip不为0则直接返回,控制只能有一个线程进入
            //未进入的线程都会将wip增加1,
            if (WIP.getAndIncrement(this) != 0) {
                return;
            }

            final Flow.Subscriber<? super T> a = actual;
            final Queue<T> q = queue;
            for (; ; ) {
                long r = requested;
                //记录当前循环消费元素个数
                long e = 0L;

                while (e != r) {
              
                    if (isCancelled()) {
//                        Operators.onDiscardQueueWithClear(q, ctx, null);
                        if (WIP.decrementAndGet(this) != 0) {
                            continue;
                        } else {
                            return;
                        }
                    }

                    boolean d = done;
                    //从队列中获取元素
                    T o =  q.poll();

                    boolean empty = o == null;

                    //队列为空且已取消直接返回
                    if (d && empty) {
                        Throwable ex = error;
                        if (ex != null) {
                            super.error(ex);
                        } else {
                            super.complete();
                        }
                        return;
                    }
                    //队列为空跳出内部循环
                    if (empty) {
                        break;
                    }
                    //将元素下发到订阅者
                    a.onNext(o);

                    e++;
                }

                if (e == r) {
                    if (isCancelled()) {
//                        Operators.onDiscardQueueWithClear(q, ctx, null);
                        if (WIP.decrementAndGet(this) != 0) {
                            continue;
                        } else {
                            return;
                        }
                    }

                    boolean d = done;

                    boolean empty = q.isEmpty();

                    if (d && empty) {
                        Throwable ex = error;
                        if (ex != null) {
                            super.error(ex);
                        } else {
                            super.complete();
                        }
                        return;
                    }
                }

                if (e != 0) {
                    //request 减去 这些消费的个数
                    Operators.produced(REQUESTED, this, e);
                }
                //持续循环,直到wip的值变为0或者被取消
                if (WIP.decrementAndGet(this) == 0) {
                    break;
                }
            }
        }
  1. 获取wip的值后并将wip的值+1;
  2. 如果wip不为0则直接返回了
  3. 下面就都是wip为0时需要执行的逻辑了
  4. 接着是将订阅者与队列赋值给本地变量,然后是一个无限的for循环,去不断的从队列中取出元素然后下发给订阅者。无限循环退出的条件就是wip为0,或者被终止了。

代码很长,其实就是做的这么个事情。很多都是判断有没有被销毁之类的异常处理。

从控制wip为0才能进入就限制住了只能有一个线程可以从队列中消费元素。所以在消费时不会有多线程访问的问题,不会破坏SpscLinkedArrayQueue的单消费者的限定规则。

BufferAsyncSink小结

它作为Flux.push使用的Subscription,使用SpscLinkedArrayQueue作为缓冲队列。所以就限定了它只能由单线程提交元素,单线程消费元素。在BufferAsyncSink只限制了消费的线程数,而提交的线程数并没有做限制。所以我们在使用Flux.push时,需要清楚下发元素是否为单线程。

写代码时一定要清楚你在做什么🤪

SerializedFluxSink源码分析

Flux.cretate使用的FluxSink是被SerializedFluxSink包装了一层的BufferAsyncSink,之后就支持多线程访问了。那它是如何做的呢?顾名思义,我们可以猜它是做了一个序列化的操作,将多个线程通过某种黑魔法变为线性的。

SerializedFluxSink成员变量
		//存放BufferAsyncSink
		final BaseSink<T> sink;
		//记录异常
        volatile Throwable error;
        //区别于BufferAsyncSink的wip,也可以理解为调用操作的总次数。
        volatile int wip;
		//区别于BufferAsyncSink的queue,自带的队列
        final Queue<T> mpscQueue;
		
        volatile boolean done;

虽然SerializedFluxSink内存有一个BufferAsyncSink,但是它并没有和它共用一个wip和queue,而是自己生成新的wip与queue。而它使用的queue也不再是SpscLinkedArrayQueue而是MpscLinkedQueue。它是一个多生产者单消费者的线程。如果不懂MpscLinkedQueue请进入:Reactor3 MpscLinkedQueue源码分析👈

SerializedFluxSink netx方法
		public FluxSink<T> next(T t) {
            Objects.requireNonNull(t, "t is null in sink.next(t)");
            if (sink.isTerminated() || done) {
                Operators.onNextDropped(t);
                return this;
            }
            //如果已经有线程进入执行下发消息,则将消息先推到本地并发的队列
            if (WIP.get(this) == 0 && WIP.compareAndSet(this, 0, 1)) {
                try {
                    sink.next(t);
                } catch (Throwable ex) {
                    Operators.onOperatorError(sink, ex, t);
                }
                //如果wip等于0说明只有这一个线程有操作元素下发,没有多余的消息了,可以直接返回。
                //wip也可以看做是最大的循环消费次数
                if (WIP.decrementAndGet(this) == 0) {
                    return this;
                }
            } else {
                //推入本地并发队列
                this.mpscQueue.offer(t);
                //如果wip大于0则返回,等于0则会进入将本地队列的消息下发的逻辑
                if (WIP.getAndIncrement(this) != 0) {
                    return this;
                }
                //该分支不从这里直接返回的条件就是wip的值为0,也就是上面分支的任务执行完毕了,
                // 在上面分支的线程直接操作sink完成后,如果有其他的线程提交了任务,它就会继续调用drainLoop,继续操作sink,
                //所以sink的生产线程是没有切换的。
                //如果没有人提交,就说明不是在高并发的情况下,直接退出。

                //在上面分支,只有当wip最后的值为0时才会直接退出,否则进入下面的drainLoop
                //也就是执行上面分支的线程正好执行完,下面分支的线程整个执行wip的get操作,顺利接棒上面的线程去操作sink,
                //控制每次只有一个线程可以操作sink
            }
            //进入到这里的wip的值不会为0,
            drainLoop();
            return this;
        }

  • 如果wip为0,则被允许直接操作BufferAsyncSink推入元素。结束后如果发现wip减一后仍为0,说明没有其他消息了则直接返回。
  • 如果wip不为0,则说明有其他线程正在操作BufferAsyncSink,说明不能再去操作BufferAsyncSink了,只能先存入本地的mpscQueue。mpscQueue它支持多线程推入消息。
  • 最后有幸运儿会调用drainLoop,堕入无限循环,而在drainLoop所做的就是将mpscQueue的消息推入BufferAsyncSink的事情。当然这个幸运儿需要控制只能由一个线程来操作。

这个方法我们就能看到SerializedFluxSink将多线程变为单线程的黑魔法。在只有单线程访问时,可以直接操作只允许单线程访问的BufferAsyncSink。通过wip判断,当有多个线程并发访问时,就先将这些元素推入一个支持多生产的队列MpscLinkedQueue将这些元素缓存在本地,然后从这些线程中选一个幸运儿去做将MpscLinkedQueue推到BufferAsyncSink的活,这样就将多个线程转换为单个线程了。

SerializedFluxSink drainLoop方法
void drainLoop() {
            BaseSink<T> e = sink;
            Queue<T> q = mpscQueue;
            for (; ; ) {

                for (; ; ) {
                    if (e.isCancelled()) {
                        Operators.onDiscardQueueWithClear(q);
                        if (WIP.decrementAndGet(this) == 0) {
                            return;
                        } else {
                            continue;
                        }
                    }

                    if (ERROR.get(this) != null) {
                        Operators.onDiscardQueueWithClear(q);
                        //noinspection ConstantConditions
                        e.error(Exceptions.terminate(ERROR, this));
                        return;
                    }

                    boolean d = done;
                    T v = q.poll();

                    boolean empty = v == null;

                    if (d && empty) {
                        e.complete();
                        return;
                    }

                    if (empty) {
                        break;
                    }

                    try {
                        e.next(v);
                    } catch (Throwable ex) {
                        Operators.onOperatorError(sink, ex, v);
                    }
                }
                //每消费完一个就将wip减一,如果等于0则说明没有消息,退出循环
                if (WIP.decrementAndGet(this) == 0) {
                    break;
                }
            }
        }

其实逻辑和BufferAsyncSink的drain方法差不多,就不再重复说明了。

总结

本篇详细的说明了造成Flux.push与Flux.create的本质原因,以及它们各自实现的源码分析。不知道读者在看的时候是否会有一些并发的疑惑,它们的并发处理有什么技巧可以供我们学习? 下一篇继续说明😄

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值