This series of articles index the "Response Spring's word Wizard"
Previously summary Spring Webflux Quick Start | Spring Webflux Performance Test
Previously summary: Reactor 3 Quick Start | Responsive Flow Specification | Custom Data Flow
This article tests the source code
2.4 Scheduler and threading model
Sections 1.3.2 briefly describe the different types of schedulers and Scheduler
how to use publishOn
and subscribeOn
switch different thread execution environments.
Here's a simple example to recall:
@Test public void testScheduling() { Flux.range(0, 10)// .log() .publishOn(Schedulers.newParallel("myParallel"))// .log() .subscribeOn(Schedulers.newElastic("myElastic")) .log() .blockLast(); }
- If you keep this log (), you can see that the source data stream is executed on the
myElastic-x
thread;
- Just keep this log () and you can see that the
publishOn
data flow is executed on the myParallel-x
thread;
- Just keep this log (), and you can see that the
subscribeOn
data flow is still executing on the myParallel-x
thread.
Through the above three log()
outputs, it can be found that for the operation chain as shown:
-
publishOn
will affect subsequent operators in the chain , such as the first Publishon adjustment Scheduler is elastic, the filter
processing operation is performed in the elastic thread pool, and the same flatMap
is done in a fixed-size parallel thread pool;
- Regardless of where
subscribeOn
it occurs, it only affects the execution environment of the source , that range
is, the method is executed in a single thread until it is first switched to the publishOn
scheduler, so range
the post is map
also executed in a single thread.
In this section we look at its implementation mechanism.
2.4.1 Scheduler
The scheduler is equivalent to Executorservice in reactor, and different schedulers define different thread execution environments. Schedulers
the static methods provided by the tool class can be used to build different thread execution environments.
Schedulers
Class has pre-created several commonly used scheduler for different thread pool models: The single()
elastic()
Scheduler created using, and parallel()
methods can use the built-in single-threaded, elastic thread pool, and fixed-size thread pools, respectively. If you want to create a new scheduler, you can use the newSingle()
, newElastic()
and newParallel()
methods. All of these methods return a Scheduler
specific implementation.
See Scheduler
What the behavior is:
public interface Scheduler extends Disposable { // 调度执行Runnable任务task。 Disposable schedule(Runnable task); // 延迟一段指定的时间后执行。 Disposable schedule(Runnable task, long delay, TimeUnit unit); // 周期性地执行任务。 Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit); // 创建一个工作线程。 Worker createWorker(); // 启动调度器 void start(); // 以下两个方法可以暂时忽略 void dispose(); long now(TimeUnit unit) // 一个Worker代表调度器可调度的一个工作线程,在一个Worker内,遵循FIFO(先进先出)的任务执行策略 interface Worker extends Disposable { // 调度执行Runnable任务task。 Disposable schedule(Runnable task); // 延迟一段指定的时间后执行。 Disposable schedule(Runnable task, long delay, TimeUnit unit); // 周期性地执行任务。 Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit); }}
, Scheduler
is the leader, Worker
is the employee, each Scheduler
hand has several Worker
. After receiving the task, Scheduler
responsible for assigning, Worker
responsible for work.
In Scheduler
, each Worker
is a ScheduledExecutorService
, or a wrapped ScheduledExecutorService
object. So, Scheduler
instead of a thread pool, you have a self-sustaining ScheduledExecutorService
pool.
The so-called "self-maintenance", there are mainly three points:
- Available for dispatch
Worker
. Schedulers.newParallel()
returned, for example ParallelScheduler
, maintains a fixed-size array, ScheduledExecutorService[]
and is ElasticScheduler
maintained by a executorservice Queue
.
- Task dispatch policy.
ElasticScheduler
and ParallelScheduler
both have a pick()
way to pick the right one Worker
.
- For the task to be processed, it is wrapped
Callable
so that it can be returned asynchronously to the Future
caller.
2.4.2 Switching the execution environment
Again back publishOn
and subscribeOn
method.
In reactor, the processing of data flows is actually a series of method invocations and event-based callbacks, including,,,, and, subscribe
onSubscribe
request
onNext
onError
onComplete
. Take out the 2.1-verse diagram to help understand:
When the .subscribe()
method is called, the data flow from upstream to downstream is formed, and the elements in the data flow are onNext* (onError|onComplete)
carried "downstream". At the same time, what reactor users cannot see is that there is also a "subscription chain" upstream from downstream, where request is being fed back to the demand along the chain.
The publishOn
method can onNext
be onError
onComplete
Scheduler
Worker
executed, dispatched to the given . So if you add one more to the middle of the scene, the .map
.filter
filtering processing of the publisheOn(Schedulers.elastic())
.filter
action will be performed on onNext
ElasticScheduler
one of the Worker
above.
subscribeOn
method is capable of subscribe
executing (calling onSubscribe
), request
dispatching to the Scheduler
given Worker
. So if you add one at any location, you can use the subscribeOn(Schedulers.elastic())
bottom-up subscription chain to subscribe()
pass the thread execution environment to the "source" and execute it on the way Flux.just
ElasticScheduler
. The subsequent operator is affected until the publishOn
execution environment is changed.
In addition, some operators themselves will require the scheduler for multi-threaded processing, when you do not explicitly specify the scheduler, those operators will use the built-in Singleton Scheduler to execute. For example, a Flux.delayElements(Duration)
Schedulers.parallel()
Scheduler object is used:
@Test public void testDelayElements() { Flux.range(0, 10) .delayElements(Duration.ofMillis(10)) .log() .blockLast(); }
From the output you can see that it onNext
is running on a different thread:
[ INFO] (main) onSubscribe(FluxConcatMap.ConcatMapImmediate)[ INFO] (main) request(unbounded)[ INFO] (parallel-1) onNext(0)[ INFO] (parallel-2) onNext(1)[ INFO] (parallel-3) onNext(2)[ INFO] (parallel-4) onNext(3)...
2.4.3 Configuring the context for a data flow
In reactor, thread-based Scheduler
scheduling is really simple and easy to use, but there is a problem that needs to be addressed.
When we were writing multithreaded code, we might use wrappers if we were involved in a value that was only used internally within the thread ThreadLocal
.
But in responsive programming, this usage loses its usefulness and even brings bugs because the threading environment is constantly changing. This is the case, for example, using the Logback MDC to store the ID of the Log Association.
Since version 3.1.0,reactor introduced an advanced feature similar to ThreadLocal: Context. It acts on a Flux or a Mono, not on a thread. That is, its life cycle accompanies the entire data stream, not the thread.
Relatively speaking, users use the context is not much, interested in or have this need, please see my translation of the relevant documents, can be reactor internal implementation, especially Subscription
have a deeper understanding.
2.4.4 Parallel execution
Nowadays multicore architectures are already popular and it is important to be able to do parallel processing conveniently.
For some tasks that can be processed sequentially in a thread, even when dispatched to Parallelscheduler, it is usually performed by only one worker, such as:
@Test public void testParallelFlux() throws InterruptedException { Flux.range(1, 10) .publishOn(Schedulers.parallel()) .log().subscribe(); TimeUnit.MILLISECONDS.sleep(10); }
The output is as follows:
[ INFO] (main) | onSubscribe([Fuseable] FluxPublishOn.PublishOnSubscriber)[ INFO] (main) | request(unbounded)[ INFO] (parallel-1) | onNext(1)[ INFO] (parallel-1) | onNext(2)[ INFO] (parallel-1) | onNext(3)[ INFO] (parallel-1) | onNext(4)[ INFO] (parallel-1) | onNext(5)[ INFO] (parallel-1) | onNext(6)[ INFO] (parallel-1) | onNext(7)[ INFO] (parallel-1) | onNext(8)[ INFO] (parallel-1) | onNext(9)[ INFO] (parallel-1) | onNext(10)[ INFO] (parallel-1) | onComplete()
Sometimes we do need tasks that are "evenly" distributed across different worker threads ParallelFlux
.
You can use the operator for any flux to parallel()
get one ParallelFlux
. However, this operator does not do parallel processing, but only divides the load into multiple execution tracks (by default, the number of tracks is equal to the number of CPU cores).
In order to configure ParallelFlux
how each track is executed in parallel, it needs to be used runOn(Scheduler)
, here, Schedulers.parallel () is the more recommended scheduler specifically for parallel processing.
@Test public void testParallelFlux() throws InterruptedException { Flux.range(1, 10) .parallel(2) .runOn(Schedulers.parallel())// .publishOn(Schedulers.parallel()) .log() .subscribe(); TimeUnit.MILLISECONDS.sleep(10); }
The output is as follows:
[ INFO] (main) onSubscribe([Fuseable] FluxPublishOn.PublishOnSubscriber)[ INFO] (main) request(unbounded)[ INFO] (main) onSubscribe([Fuseable] FluxPublishOn.PublishOnSubscriber)[ INFO] (main) request(unbounded)[ INFO] (parallel-1) onNext(1)[ INFO] (parallel-2) onNext(2)[ INFO] (parallel-1) onNext(3)[ INFO] (parallel-2) onNext(4)[ INFO] (parallel-1) onNext(5)[ INFO] (parallel-2) onNext(6)[ INFO] (parallel-1) onNext(7)[ INFO] (parallel-2) onNext(8)[ INFO] (parallel-1) onNext(9)[ INFO] (parallel-2) onNext(10)[ INFO] (parallel-1) onComplete()[ INFO] (parallel-2) onComplete()
As you can see, the OnNext "uniform" distribution of individual elements is performed on two threads, with separate events on each thread, onComplete
which publishOn
differs from scheduling to Parallelscheduler.
Reactor Scheduler and threading model--Response Spring's Path wizard