Использование RxJava позволяет легко добиться переключения потоков, поэтому его часто используют для замены собственных классов инструментов, таких как AsyncTask и Handler в Android. Хотя он прост в использовании, если вы не понимаете основных принципов, лежащих в его основе, вы можете написать ошибку из-за неправильного использования. В этой статье вы сможете кратко понять принцип реализации переключения потоков RxJava и меры предосторожности при разработке.
1. Основное использование
- Планировщик
Если вы хотите внедрить многопоточность в свой каскад наблюдаемых операторов, вы можете сделать это, указав этим операторам работать с определенными планировщиками.
Через планировщик разрешите операторам работать в указанном потоке, чтобы добиться многопоточного планирования.
- наблюдатель
укажите планировщик, на котором наблюдатель будет наблюдать за этим
наблюдаемым
- подписаться
укажите планировщик, на котором Observable будет работать
самостоятельно, на котором будет выполняться указанный планировщик Observable
Каждый оператор в цепочке вызовов RxJava создаст новый Observable, а новый Observable, сгенерированный оператором, зарегистрирует обратный вызов для верхнего Observable. Принцип реализации subscribeOn и ObservationOn одинаков:
- subscribeOn подписывается на восходящий поток в указанном потоке (настройте метод подписки восходящего потока в указанном потоке)
- ObserveOn вызывает нижестоящие методы обратного вызова (onNext / onError / onComplete и т. Д.) В указанном потоке после получения данных.
RxJava устанавливает подписку снизу вверх, а затем передает данные сверху вниз, поэтому, даже если subscribeOn появляется после наблюденияOn, он может гарантировать выполнение потока источника данных, потому что подписка всегда выполняется первой.
2. подписаться на
2.1 Принцип реализации
Узнайте об основном принципе subscribeOn для переключения потоков с помощью исходного кода.
//ObservableSubscribeOn.java
final class ObservableSubscribeOn extends Observable<T> {
@Override
public void subscribeActual(final Observer<? super T> s) {
final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(s);
s.onSubscribe(parent);
// 没有直接调用subscribe订阅,而是先进行了线程变换(scheduler.scheduleDirect)
parent.setDisposable(
scheduler.scheduleDirect(new SubscribeTask(parent)));
}
final class SubscribeTask implements Runnable {
@Override
public void run() {
// run()会在指定的scheduler调用,向上游订阅时线程已经发生了变化
// 所以保证了上游所运行的线程
source.subscribe(parent);
}
}
static final
class SubscribeOnObserver<T> implements Observer<T>, Disposable {
@Override
public void onNext(T t) {
// 收到数据后不进行线程变换
actual.onNext(t);
}
}
}
2.2 subscribeOn вступает в силу только один раз
subscribeOn изменяет Observable.create
поток , переключая поток подписки, тем самым влияя на поток передачи данных.
Поскольку процесс подписки идет снизу вверх, на Observable.create влияет только самый последний subscribeOn. Если в цепочке вызовов есть несколько subscribeOn, действителен только первый. Другой subscibeOn все еще может влиять на свой восходящий doOnSubscribe
поток выполнения.
@Test
fun test() {
Observable.create<Unit> { emitter ->
log("onSubscribe")
emitter.onNext(Unit)
emitter.onComplete()
}.subscribeOn(namedScheduler("1 - subscribeOn"))
.doOnSubscribe { log("1 - doOnSubscribe") }
.subscribeOn(namedScheduler("2 - subscribeOn"))
.doOnSubscribe { log("2 - doOnSubscribe") }
.doOnNext { log("onNext") }
.test().awaitTerminalEvent() // Wait until observable completes
}
2.3 Правильно понять значение слова subscribeOn
Несмотря на то, что мы добавили .subscribeOn (), этого недостаточно. Оператор SubscribeOn только переключает процесс подписки на желаемый поток, но это не означает, что элементы будут отправлены в этом потоке.
SubscribeOn используется для определения потока подписки, но это не означает, что данные восходящего потока должны поступать из этого потока
@Test
fun test() {
val observable = Observable.create<Int> { emitter ->
log("onSubscribe")
thread(name = "Main thread", isDaemon = false) {
log("1 - emitting"); emitter.onNext(1)
log("2 - emitting"); emitter.onNext(2)
log("3 - emitting"); emitter.onNext(3)
emitter.onComplete()
}
}
observable
.subscribeOn(Schedulers.computation())
.doOnNext { log("$it - after subscribeOn") }
.test().awaitTerminalEvent() // Wait until observable completes
}
Правильное понимание значения subscribeOn помогает избежать некоторых недоразумений при использовании:
Недействительно для PublishSubject
@Test
fun test() {
val subject = PublishSubject.create<Int>()
val observer1 = subject
.subscribeOn(Schedulers.io())
.doOnNext { log("$it - I want this happen on an IO thread") }
.test()
val observer2 = subject
.subscribeOn(Schedulers.newThread())
.doOnNext { log("$it - I want this happen on a new thread") }
.test()
sleep(10);
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
subject.onComplete()
observer1.awaitTerminalEvent()
observer2.awaitTerminalEvent()
}
Для PublishSubject, из какого потока поступают данные восходящего потока, определяется onNext, поэтому нет смысла использовать subscribeOn для PublishSubject.
Недействительно для Observable.just ()
Обычно subcribeOn может определять поток выполнения Observable.create {...}, поэтому многие новички могут совершить ошибку, выполняя трудоемкие задачи в Observable.just (...) и ошибочно полагая, что они будут выполняться в теме subscribeOn:
Как и выше, readFromDb()
вставлять просто. just () выполняется немедленно в текущем потоке, поэтому на него не влияет subscribeOn, и его следует изменить следующим образом:
//Observable.defer
Observable.defer { Observable.just(readFromDb()) }
.subscribeOn(Schedulers.io())
.subscribe { ... }
//Observable.fromCallable
Observable.fromCallable { readFromDb() }
.subscribeOn(Schedulers.io())
.subscribe { ... }
Используйте flatMap для обработки параллелизма
Текущая подписка на Observable определяется параметром subscribeOn, поэтому обратите особое внимание на использование flatMap.
Observable.fromIterable(listOf("id1", "id2", "id3"))
.flatMap { id -> loadData(id) }
.subscribeOn(Schedulers.io())
.observeOn(mainThread())
.toList()
.subscribe { result -> log(result) }
Если нам нужно несколько loadData(id)
одновременных выполнений, вышеприведенное описание неверно.
subscribeOn определяет восходящий поток flatMap. FlatMap возвращает несколько наблюдаемых подписок, которые происходят в этом потоке. Множественные подписки loadData
могут выполняться только в одном потоке и не могут быть распараллелены.
Чтобы добиться эффекта параллельного выполнения, вам необходимо изменить следующее:
Observable.fromIterable(listOf("id1", "id2", "id3"))
.flatMap { id ->
loadData(id)
.subscribeOn(Schedulers.io())
}
.observeOn(mainThread())
.toList()
.subscribe { result -> log(result) }
3. наблюдать за
3.1 Принцип реализации
Из исходного кода, чтобы понять основной принцип наблюдения за переключением потоков.
//ObservableObserveOn.java
final class ObservableObserveOn extends Observable<T> {
@Override
protected void subscribeActual(Observer<? super T> observer) {
if (scheduler instanceof TrampolineScheduler) {
source.subscribe(observer);
} else {
Scheduler.Worker w = scheduler.createWorker();
// 直接向上游订阅数据,不进行线程切换,切换操作在Observer中进行
source.subscribe(
new ObserveOnObserver<T>(observer, w, delayError, bufferSize));
}
}
static final class ObserveOnObserver<T> implements Observer<T>, Runnable {
@Override
public void onNext(T t) {
if (done) {
return;
}
// 这里选把数据放到队列中,增加吞吐量,提高性能
if (sourceMode != QueueDisposable.ASYNC) {
queue.offer(t);
}
// 在schedule方法里进行线程切换并把数据循环取出
// 回调给下游,下游会在指定的线程中收到数据
schedule();
}
void schedule() {
if (this.getAndIncrement() == 0) {
//切换线程
this.worker.schedule(this);
}
}
}
}
3.2 наблюдениеOn действует несколько раз
В отличие от subscribeOn, ObservationOn может иметь несколько значений, и каждый из них будет действовать
- За потоком, переключенным с помощью subscribeOn, можно следить через doOnSubscribe.
- Наблюдение за переключением потоков может прослушивать через doOnNext
3.3 Можно ли гарантировать сериализацию при непрерывной передаче нескольких элементов?
После того, как наблюдениеOn использует Планировщик для планирования потоков, будет ли следующий поток выполняться в одном потоке или в нескольких потоках? Можете ли вы гарантировать упорядоченность исходящих данных?
@Test
fun test() {
Observable.create<Int> { emitter ->
repeat(10) {
emitter.onNext(it)
}
emitter.onComplete()
}.observeOn(Schedulers.io())
.subscribe {
log(" - $it")
}
}
Из результатов видно, что даже после планирования планировщиком нисходящий поток все еще работает в одном потоке, что может обеспечить порядок данных во всей цепочке вызовов.
Так почему же все они выполняются в одном потоке после того, как запланированы планировщиком?
4. Планировщик
4.1 Принцип реализации
Scheculer не планирует напрямую Runnable, но создает Worker, а затем Worker планирует определенные задачи.
SubscribeTask
И subscribeOn, и exploreOn ObserveOnObserver
реализуют Runnable, поэтому в конечном итоге они выполняются в Worker.
4.2 Задачи планируются работником
Один планировщик может создавать несколько рабочих, а один рабочий может управлять несколькими задачами (выполняемый)
Рабочие существуют для того, чтобы гарантировать две вещи:
- Задачи, созданные одним и тем же воркером, обеспечивают последовательное выполнение, а задачи, выполняемые немедленно, соответствуют принципу «первым пришел - первым обслужен».
- Worker привязан к Runnable, который вызывает его метод. Когда Worker отменяется, все задачи, основанные на нем, отменяются.
4.3 Как Worker гарантирует сериализацию?
Очень просто, у каждого воркера только один поток
Теперь мы можем ответить на вопрос: почему наблюдениеOn все еще выполняется в одном потоке после того, как планировщик запланировал это?
Планировщик назначает уникальный воркер каждому наблюдателю, поэтому последующий поток наблюдения может гарантировать последовательное выполнение в одном потоке.
//ObservableObserveOn.java
final class ObservableObserveOn extends Observable<T> {
@Override
protected void subscribeActual(Observer<? super T> observer) {
if (scheduler instanceof TrampolineScheduler) {
source.subscribe(observer);
} else {
Scheduler.Worker w = scheduler.createWorker();
source.subscribe(
new ObserveOnObserver<T>(observer, w, delayError, bufferSize)); //传入worker
}
}
...
}
Как и выше, Worker хранится как переменная-член ObserveOnObserver
4.4 Предустановленные планировщики
Точно так же, как Executors предоставляет множество ThreadPoolExecutors, Schedulers предоставляет множество предустановленных планировщиков
Нет. | Планировщики и описания |
---|---|
1 | Schedulers.single () - это глобально уникальный поток, независимо от того, сколько существует наблюдаемых объектов, все они используют этот уникальный поток. |
2 | Schedulers.io () - один из наиболее распространенных планировщиков, используемых для операций, связанных с вводом-выводом, таких как сетевые запросы и файловые операции. Планировщик ввода-вывода поддерживается пулом потоков. Сначала он создает рабочий поток, который можно повторно использовать для других операций.Когда этот рабочий поток (в случае долгосрочной задачи) не может быть повторно использован, создается новый поток для обработки других операций. |
3 | Schedulers.computation () очень похож на планировщик ввода-вывода и также реализован на основе пула потоков. Количество доступных потоков фиксировано и соответствует количеству ядер процессора. Когда все потоки заняты, новая задача может находиться только в состоянии ожидания. Следовательно, он не подходит для операций, связанных с вводом-выводом. Он подходит для некоторых расчетных операций, и одна вычислительная задача не будет долго занимать потоки. |
4 | Schedulers.newThread () создает новый поток каждый раз, когда он вызывается |
5 | Schedulers.trampoline () выполняется в текущем потоке без переключения потоков. |
6 | Schedulers.from (исполнитель java.util.concurrent.Executor) больше похож на настраиваемый планировщик ввода-вывода. Мы можем создать собственный пул потоков, указав размер пула потоков. Количество наблюдаемых подходит для использования в сценариях со слишком большим количеством планировщиков ввода-вывода, |
//Sample of Schedulers.from
fun namedScheduler(name: String): Scheduler {
return Schedulers.from(
Executors.newCachedThreadPool { Thread(it, name) }
)
}
Безопасность потоков
5.1 Являются ли операторы RxJava потокобезопасными?
@Test
fun test() {
val numberOfThreads = 1000
val publishSubject = PublishSubject.create<Int>()
val actuallyReceived = AtomicInteger()
publishSubject
.take(300).subscribe {
actuallyReceived.incrementAndGet()
}
val latch = CountDownLatch(numberOfThreads)
var threads = listOf<Thread>()
(0..numberOfThreads).forEach {
threads += thread(start = false) {
publishSubject.onNext(it)
latch.countDown()
}
}
threads.forEach { it.start() }
latch.await()
val sum = actuallyReceived.get()
check(sum == 300) { "$sum != 300" }
}
Результат не такой, как ожидалось, потому что он take
не ориентирован на многопоточность
Взгляните на исходный код take
public final class ObservableTake<T> extends AbstractObservableWithUpstream<T, T> {
final long limit;
public ObservableTake(ObservableSource<T> source, long limit) {
super(source);
this.limit = limit;
}
protected void subscribeActual(Observer<? super T> observer) {
this.source.subscribe(new ObservableTake.TakeObserver(observer, this.limit));
}
static final class TakeObserver<T> implements Observer<T>, Disposable {
final Observer<? super T> downstream;
boolean done;
Disposable upstream;
long remaining;
TakeObserver(Observer<? super T> actual, long limit) {
this.downstream = actual;
this.remaining = limit;
}
public void onNext(T t) {
if (!this.done && this.remaining-- > 0L) {
boolean stop = this.remaining == 0L;
this.downstream.onNext(t);
if (stop) {
this.onComplete();
}
}
}
}
}
Право по сигналу remaining--
- это не заблокированная операция
5.2 Потоковая безопасность observableOn
Затем, если добавлен observableOn, сериализация гарантирована, потому что take может выполняться в одном потоке
@Test
fun test() {
repeat(10000) {
val numberOfThreads = 1000
val publishSubject = PublishSubject.create<Int>()
val actuallyReceived = AtomicInteger()
publishSubject
.observeOn(Schedulers.io())
.take(300).subscribe {
actuallyReceived.incrementAndGet()
}
val latch = CountDownLatch(numberOfThreads)
var threads = listOf<Thread>()
(0..numberOfThreads).forEach {
threads += thread(start = false) {
publishSubject.onNext(it)
latch.countDown()
}
}
threads.forEach { it.start() }
latch.await()
check(actuallyReceived.get() == 300)
}
}
К сожалению, после его многократного запуска по-прежнему возникают проблемы, потому что сам observableOn не является потокобезопасным, а ObservableOn использует queue
небезопасную очередь.
5.3 Наблюдаемый контракт
Rx ясно сказал нам в определении Observable:
Наблюдаемые объекты должны отправлять уведомления наблюдателям последовательно (не параллельно). Они могут отправлять эти уведомления из разных потоков, но между уведомлениями должна быть формальная связь «происходит до».
reactivex.io/documentati…
В заключение, операторы RxJava по умолчанию не являются потокобезопасными .
Но операторы, которые получают несколько Observable, такие как merge (), combLatest (), zip () и т. Д., Являются потокобезопасными, поэтому даже если несколько Observable поступают из разных потоков, вам не нужно учитывать проблемы безопасности потоков.