文章目录
系列文章
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
方法的步骤:
- 先创建了一个
BaseSink
。push
与create
都是使用的默认的BufferAsyncSink
。这里就不放代码了,里面逻辑不复杂。 - 将
BaseSin
k当做一个Subscription
传给Subscriber
,作为FluxCreatepush
与create
都是使用的默认的BufferAsyncSink
。与Subscriber
的桥梁。 - 接着就是黑魔法啦,通过
CreateMode
模式的不同,来对create
的sink
使用SerializedFluxSink
多进行了一层封装。
可以看到最先用到的是BaseSink然后是BufferAsyncSink与SerializedFluxSink,我们就根究这个顺序来逐个分析吧。
BaseSink源码分析
在上一篇中我们看到了BaseSink是实现了FluxSink与Subscription两个接口,充当FluxCreate与Subscriber直接的桥梁。
BaseSink它是一个抽象类,其他的Sink继承它。它实现了一些通用的方法比如request
,complete
,error
,cancel
等。而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;
}
}
}
- 获取wip的值后并将wip的值+1;
- 如果wip不为0则直接返回了
- 下面就都是wip为0时需要执行的逻辑了
- 接着是将订阅者与队列赋值给本地变量,然后是一个无限的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的本质原因,以及它们各自实现的源码分析。不知道读者在看的时候是否会有一些并发的疑惑,它们的并发处理有什么技巧可以供我们学习? 下一篇继续说明😄