how to perform two operations on the same flux [duplicate] - project-reactor

I have streams of incoming events that need to be enriched, and then processed in parallel as they arrive.
I was thinking Project Reactor was made to order for the job, but in my tests all of the processing seems to be done serially.
Here is some test code:
ExecutorService executor = Executors.newFixedThreadPool(10);
System.out.println("Main thread: " + Thread.currentThread());
Flux<String> tick = Flux.interval(Duration.of(10, ChronoUnit.MILLIS))
.map(i-> {
System.out.println("ReactorTests.test " + Thread.currentThread());
sleep(1000L); // simulate IO delay
return String.format("String %d", i);
})
.take(3)
// .subscribeOn(Schedulers.elastic());
// .subscribeOn(Schedulers.newParallel("test"));
// .subscribeOn(Schedulers.fromExecutor(executor));
;
tick.subscribe(x ->System.out.println("Subscribe thread: " + Thread.currentThread()),
System.out::println,
()-> System.out.println("Done"));
System.out.println("DONE AND DONE");
I have tried uncommenting each of the commented lines, however in every case the output indicates that the same thread is used to process all of the events
Main thread: Thread[main,5,main]
[DEBUG] (main) Using Console logging
DONE AND DONE
ReactorTests.test Thread[parallel-1,5,main]
Subscribe thread: Thread[parallel-1,5,main]
ReactorTests.test Thread[parallel-1,5,main]
Subscribe thread: Thread[parallel-1,5,main]
ReactorTests.test Thread[parallel-1,5,main]
Subscribe thread: Thread[parallel-1,5,main]
Done
(The only difference is that without the Schedulers, they are run on the subscribe thread, whereas with any of the executors, they all run in the same thread, which is not the subscribe thread.)
What am I missing?
FYI, there is a "sleep" method:
public static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
System.out.println("Exiting");
}
}

One way to handle items in parallel, is to use .parallel / .runOn
flux
.parallel(10)
.runOn(scheduler)
//
// Work to be performed in parallel goes here. (e.g. .map, .flatMap, etc)
//
// Then, if/when you're ready to go back to sequential, call .sequential()
.sequential()
Blocking operations (such as blocking IO, or Thread.sleep) will block the thread on which they are executed. Reactive streams cannot magically turn a blocking method into a non-blocking method. Therefore, you need to ensure blocking methods are run on a Scheduler suitable for blocking operations (e.g. Schedulers.boundedElastic()).
In the example above, since you know you are calling a blocking operation, you could use .runOn(Schedulers.boundedElastic()).
Depending on the use case, you can also use async operators like .flatMap in combination with .subscribeOn or .publishOn to delegate specific blocking operations to another Scheduler, as described in the project reactor docs. For example:
flux
.flatMap(i -> Mono.fromCallable(() -> {
System.out.println("ReactorTests.test " + Thread.currentThread());
sleep(1000L); // simulate IO delay
return String.format("String %d", i);
})
.subscribeOn(Schedulers.boundedElastic()))
In fact, .flatMap also has an overloaded variant that takes a concurrency parameter where you can limit the maximum number of in-flight inner sequences. This can be used instead of .parallel in some use cases. It will not generally work for Flux.interval though, since Flux.interval doesn't support downstream requests that replenish slower than the ticks.

I will use Flux.range(1, 100) as the source of event in this demo;
change map() to flatMap() with a concurrency 3;
inside flatMap(), use Mono.fromCallable() to wrap your "IO delay code";
make the Mono.fromCallable to "subscribeOn" a Scheduler;
here is the code:
#Test
void testParallelEvent(){
System.out.println("Main thread: " + Thread.currentThread());
Flux<String> tick = ⚠️Flux.range(1, 100)
.⚠️flatMap(i-> ⚠️Mono.fromCallable(()->{
System.out.println("ReactorTests.test " + Thread.currentThread());
sleep(500L); // simulate IO delay
return String.format("String %d", i);
}).⚠️subscribeOn(Schedulers.boundedElastic())
, ⚠️3) // concurrency
.take(6);
// .subscribeOn(Schedulers.elastic());
// .subscribeOn(Schedulers.newParallel("test"));
// .subscribeOn(Schedulers.fromExecutor(executor));
tick.subscribe(x ->System.out.println("Subscribe thread: " + Thread.currentThread() + " --> " + x),
System.out::println,
()-> System.out.println("Done"));
System.out.println("DONE AND DONE");
sleep(8000);
}
and the output is:
Main thread: Thread[main,5,main]
16:35:34.034 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
ReactorTests.test Thread[boundedElastic-1,5,main]
ReactorTests.test Thread[boundedElastic-2,5,main]
DONE AND DONE
ReactorTests.test Thread[boundedElastic-3,5,main]
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 1
Subscribe thread: Thread[boundedElastic-3,5,main] --> String 3
ReactorTests.test Thread[boundedElastic-4,5,main]
ReactorTests.test Thread[boundedElastic-5,5,main]
Subscribe thread: Thread[boundedElastic-2,5,main] --> String 2
ReactorTests.test Thread[boundedElastic-3,5,main]
Subscribe thread: Thread[boundedElastic-4,5,main] --> String 4
ReactorTests.test Thread[boundedElastic-2,5,main]
Subscribe thread: Thread[boundedElastic-5,5,main] --> String 5
ReactorTests.test Thread[boundedElastic-4,5,main]
Subscribe thread: Thread[boundedElastic-3,5,main] --> String 6
Done
Exiting
Exiting
if you need to use Flux.interval() as the event source, you must add some backpressure strategy or otherwise end up with the OverflowException:
Flux.interval(Duration.ofMillis(10))
.onBackpressureBuffer(10) // backpressure strategy
.flatMap(...)
here is full source code with Flux.interval & onBackpressureBuffer:
#Test
void testParallelWithBackpressureBuffer(){
System.out.println("Main thread: " + Thread.currentThread());
Flux<String> tick = Flux.interval(Duration.ofMillis(10))
.onBackpressureBuffer(10) // ⚠️backpressure strategy
.flatMap(i-> Mono.fromCallable(()->{
System.out.println("simulate IO " + Thread.currentThread() + " " + i);
sleep(1000L); // simulate IO delay, very slow
return String.format("String %d", i);
}).subscribeOn(Schedulers.boundedElastic())
, 3)
.take(10);
Disposable disposable = tick.subscribe(x ->System.out.println("Subscribe thread: " + Thread.currentThread() + " --> " + x),
System.out::println,
()-> System.out.println("Done"));
while(!disposable.isDisposed()){
sleep(800);
System.out.println("..wait..");
}
System.out.println("DONE AND DONE");
}
and the result will be
Main thread: Thread[main,5,main]
15:08:52.854 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
simulate IO Thread[boundedElastic-1,5,main] 0
simulate IO Thread[boundedElastic-2,5,main] 1
simulate IO Thread[boundedElastic-3,5,main] 2
..wait..
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 0
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 1
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 2
simulate IO Thread[boundedElastic-4,5,main] 3
simulate IO Thread[boundedElastic-2,5,main] 4
simulate IO Thread[boundedElastic-3,5,main] 5
..wait..
Subscribe thread: Thread[boundedElastic-4,5,main] --> String 3
simulate IO Thread[boundedElastic-1,5,main] 6
Subscribe thread: Thread[boundedElastic-2,5,main] --> String 4
Subscribe thread: Thread[boundedElastic-2,5,main] --> String 5
simulate IO Thread[boundedElastic-3,5,main] 7
simulate IO Thread[boundedElastic-4,5,main] 8
..wait..
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 6
simulate IO Thread[boundedElastic-2,5,main] 9
Subscribe thread: Thread[boundedElastic-3,5,main] --> String 7
Subscribe thread: Thread[boundedElastic-3,5,main] --> String 8
simulate IO Thread[boundedElastic-4,5,main] 10
simulate IO Thread[boundedElastic-1,5,main] 11
..wait..
..wait..
Subscribe thread: Thread[boundedElastic-2,5,main] --> String 9
Done
Exiting
Exiting
..wait..
DONE AND DONE

Related

Combine's receive(on:) not dispatching to serial queue, causing data race

According to Apple, receive(on:options:) runs callbacks on a given queue. We use a serial dispatch queue to prevent racing on localOptionalCancellable in the code below. But receiveCancel is not getting dispatched to that queue. Can someone tell me why?
From the documentation,
You use the receive(on:options:) operator to receive results and completion on a specific scheduler, such as performing UI work on the main run loop.
...
Prefer receive(on:options:) over explicit use of dispatch queues when performing work in subscribers. For example, instead of the following pattern:
Issue Reproduction:
import Foundation
import Combine
class Example {
private var localOptionalCancellable: AnyCancellable?
private let dispatchQueue = DispatchQueue(label: "LocalQueue-\(UUID())")
func misbehavingFunction() {
self.dispatchQueue.async {
self.localOptionalCancellable = Just(())
.setFailureType(to: Error.self)
.receive(on: self.dispatchQueue)
.handleEvents(
receiveCancel: {
// Simultaneous accesses to 0x600000364e10, but modification requires exclusive access.
// Can be fixed by wrapping in self.dispatchQueue.async {}
self.localOptionalCancellable = nil
}
)
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in
self.localOptionalCancellable = nil
}
)
}
}
}
Example().misbehavingFunction()
Stack Trace:
Simultaneous accesses to 0x600000364e10, but modification requires exclusive access.
Previous access (a modification) started at (0x10eeaf12a).
Current access (a modification) started at:
0 libswiftCore.dylib 0x00007fff2ff7be50 swift_beginAccess + 568
3 Combine 0x00007fff4ba73a40 Publishers.HandleEvents.Inner.cancel() + 71
4 Combine 0x00007fff4ba74230 protocol witness for Cancellable.cancel() in conformance Publishers.HandleEvents<A>.Inner<A1> + 16
5 Combine 0x00007fff4b9f10c0 Subscribers.Sink.cancel() + 652
6 Combine 0x00007fff4b9f1500 protocol witness for Cancellable.cancel() in conformance Subscribers.Sink<A, B> + 16
7 Combine 0x00007fff4b9dd2d0 AnyCancellable.cancel() + 339
8 Combine 0x00007fff4b9dd5f0 AnyCancellable.__deallocating_deinit + 9
9 libswiftCore.dylib 0x00007fff2ff7da20 _swift_release_dealloc + 16
13 Combine 0x00007fff4b9f0da0 Subscribers.Sink.receive(_:) + 54
14 Combine 0x00007fff4b9f14c0 protocol witness for Subscriber.receive(_:) in conformance Subscribers.Sink<A, B> + 16
15 Combine 0x00007fff4ba73ed0 Publishers.HandleEvents.Inner.receive(_:) + 129
16 Combine 0x00007fff4ba74170 protocol witness for Subscriber.receive(_:) in conformance Publishers.HandleEvents<A>.Inner<A1> + 16
17 Combine 0x00007fff4ba26440 closure #1 in Publishers.ReceiveOn.Inner.receive(_:) + 167
18 libswiftDispatch.dylib 0x000000010e97cad0 thunk for #escaping #callee_guaranteed () -> () + 14
19 libdispatch.dylib 0x00007fff20105323 _dispatch_call_block_and_release + 12
20 libdispatch.dylib 0x00007fff20106500 _dispatch_client_callout + 8
21 libdispatch.dylib 0x00007fff2010c12e _dispatch_lane_serial_drain + 715
22 libdispatch.dylib 0x00007fff2010cde1 _dispatch_lane_invoke + 403
23 libdispatch.dylib 0x00007fff20117269 _dispatch_workloop_worker_thread + 782
24 libsystem_pthread.dylib 0x00007fff6116391b _pthread_wqthread + 290
25 libsystem_pthread.dylib 0x00007fff61162b68 start_wqthread + 15
Fatal access conflict detected.
According to Apple, receive(on:options:) runs callbacks on a given queue.
Not exactly. Here's what the documentation actually says:
You use the receive(on:options:) operator to receive results and completion on a specific scheduler, such as performing UI work on the main run loop. In contrast with subscribe(on:options:), which affects upstream messages, receive(on:options:) changes the execution context of downstream messages.
(Emphasis added.) So receive(on:) controls the Scheduler used to call a Subscriber's receive(_:) and receive(completion:) methods. It does not control the Scheduler used to call the Subscription's request(_:) or cancel() methods.
To control the Scheduler used to call the Subscription's cancel() method, you need to use the subscribe(on:options:) operator downstream of the handleEvents operator, like this:
self.localOptionalCancellable = Just(())
.setFailureType(to: Error.self)
.receive(on: self.dispatchQueue)
.handleEvents(
receiveCancel: {
// Simultaneous accesses to 0x600000364e10, but modification requires exclusive access.
// Can be fixed by wrapping in self.dispatchQueue.async {}
self.localOptionalCancellable = nil
}
)
.subscribe(on: self.dispatchQueue)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.sink(
receiveCompletion: { _ in },
receiveValue: { _ in
self.localOptionalCancellable = nil
}
)

#synchronized the writing queue makes thread safe?

NSObject *token = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
#synchronized (token) {
array = [NSArray arrayWithObject:#"1"];
}
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
NSLog(#"%#",array);
}
});
This code won't crash, even there is no #synchronized in the read queue.
As compared,
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
[lock lock];
array = [NSArray arrayWithObject:#"1"];
[lock unlock];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
NSLog(#"%#",array);
}
});
This code will crash as array is not thread safe. How does this one-side #synchronized keeps the thread safe?
How does this one-side #synchronized keeps the thread safe?
It doesn’t.
Using #synchronized (or any synchronization method) for writes, but not for reads, is not thread-safe. Just because your code does not readily crash does not mean it is thread-safe. Both reads and writes must be synchronized. Neither of these two examples, with unsynchronized reads, is thread-safe.
If you want to test for thread-safety, consider the thread sanitizer (TSAN). For example, when I ran the #synchronized example through TSAN, it reported:
WARNING: ThreadSanitizer: data race (pid=89608)
Read of size 8 at 0x7b0c0007bce8 by thread T1:
#0 __29-[ViewController viewDidLoad]_block_invoke.14 <null> (MyApp12:x86_64+0x100002e3d)
#1 __tsan::invoke_and_release_block(void*) <null> (libclang_rt.tsan_osx_dynamic.dylib:x86_64h+0x74d7b)
#2 _dispatch_client_callout <null> (libdispatch.dylib:x86_64+0x40af)
Previous write of size 8 at 0x7b0c0007bce8 by thread T3 (mutexes: write M1679):
#0 __29-[ViewController viewDidLoad]_block_invoke <null> (MyApp12:x86_64+0x100002c02)
#1 __tsan::invoke_and_release_block(void*) <null> (libclang_rt.tsan_osx_dynamic.dylib:x86_64h+0x74d7b)
#2 _dispatch_client_callout <null> (libdispatch.dylib:x86_64+0x40af)
Location is heap block of size 48 at 0x7b0c0007bcc0 allocated by main thread:
#0 malloc <null> (libclang_rt.tsan_osx_dynamic.dylib:x86_64h+0x5239a)
#1 _Block_object_assign <null> (libsystem_blocks.dylib:x86_64+0x14fc)
#2 _Block_copy <null> (libsystem_blocks.dylib:x86_64+0x141c)
#3 -[ViewController viewDidLoad] <null> (MyApp12:x86_64+0x100002878)
#4 -[NSViewController _sendViewDidLoad] <null> (AppKit:x86_64+0xb41ce)
#5 start <null> (libdyld.dylib:x86_64+0x15620)
Mutex M1679 (0x7b0400004020) created at:
#0 objc_sync_enter <null> (libclang_rt.tsan_osx_dynamic.dylib:x86_64h+0x72a45)
#1 __29-[ViewController viewDidLoad]_block_invoke <null> (MyApp12:x86_64+0x100002b51)
#2 __tsan::invoke_and_release_block(void*) <null> (libclang_rt.tsan_osx_dynamic.dylib:x86_64h+0x74d7b)
#3 _dispatch_client_callout <null> (libdispatch.dylib:x86_64+0x40af)
Thread T1 (tid=3657833, running) is a GCD worker thread
Thread T3 (tid=3657837, running) is a GCD worker thread
SUMMARY: ThreadSanitizer: data race (/Users/.../Library/Developer/Xcode/DerivedData/MyApp-ewpoprpgpjgbmrcrkyycvogiazhl/Build/Products/Debug/MyApp12.app/Contents/MacOS/MyApp:x86_64+0x100002e3d) in __29-[ViewController viewDidLoad]_block_invoke.14+0x6d
Whichever synchronization mechanism you use (whether #synchronized, locks, or GCD), both reads and writes must be synchronized. Data race problems are notoriously difficult to manifest, and, as such, one should hesitate to draw any conclusions from an absence of a crash.
Why does it work? Without going into technical details, and really just guessing, I think this has to do with the way in which the multithreading is implemented.
Reason is based on the following, which will also NOT crash
NSLock * lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
int i = 0;
while (1) {
lock.lock;
// Do some work
for ( int j = 0; j < 10000; j ++ )
{
double x = atan ( j );
}
array = [NSArray arrayWithObject:#( i )];
lock.unlock;
i ++;
}
});
This is essentially the same as your code, but inside the lock I do some serious work. I think that work is 'hard' enough for the dispatcher to pause the other thread and I think once that work is done, the multithreading dispatcher says enough, time for some other threads, and then this one in turn is put on hold long enough for the other one to finish unmolested.
PS : That said, I think you will get different results as the load on the system changes and you need to properly sync access.

How do I process Flux events in parallel to each other?

I have streams of incoming events that need to be enriched, and then processed in parallel as they arrive.
I was thinking Project Reactor was made to order for the job, but in my tests all of the processing seems to be done serially.
Here is some test code:
ExecutorService executor = Executors.newFixedThreadPool(10);
System.out.println("Main thread: " + Thread.currentThread());
Flux<String> tick = Flux.interval(Duration.of(10, ChronoUnit.MILLIS))
.map(i-> {
System.out.println("ReactorTests.test " + Thread.currentThread());
sleep(1000L); // simulate IO delay
return String.format("String %d", i);
})
.take(3)
// .subscribeOn(Schedulers.elastic());
// .subscribeOn(Schedulers.newParallel("test"));
// .subscribeOn(Schedulers.fromExecutor(executor));
;
tick.subscribe(x ->System.out.println("Subscribe thread: " + Thread.currentThread()),
System.out::println,
()-> System.out.println("Done"));
System.out.println("DONE AND DONE");
I have tried uncommenting each of the commented lines, however in every case the output indicates that the same thread is used to process all of the events
Main thread: Thread[main,5,main]
[DEBUG] (main) Using Console logging
DONE AND DONE
ReactorTests.test Thread[parallel-1,5,main]
Subscribe thread: Thread[parallel-1,5,main]
ReactorTests.test Thread[parallel-1,5,main]
Subscribe thread: Thread[parallel-1,5,main]
ReactorTests.test Thread[parallel-1,5,main]
Subscribe thread: Thread[parallel-1,5,main]
Done
(The only difference is that without the Schedulers, they are run on the subscribe thread, whereas with any of the executors, they all run in the same thread, which is not the subscribe thread.)
What am I missing?
FYI, there is a "sleep" method:
public static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
System.out.println("Exiting");
}
}
One way to handle items in parallel, is to use .parallel / .runOn
flux
.parallel(10)
.runOn(scheduler)
//
// Work to be performed in parallel goes here. (e.g. .map, .flatMap, etc)
//
// Then, if/when you're ready to go back to sequential, call .sequential()
.sequential()
Blocking operations (such as blocking IO, or Thread.sleep) will block the thread on which they are executed. Reactive streams cannot magically turn a blocking method into a non-blocking method. Therefore, you need to ensure blocking methods are run on a Scheduler suitable for blocking operations (e.g. Schedulers.boundedElastic()).
In the example above, since you know you are calling a blocking operation, you could use .runOn(Schedulers.boundedElastic()).
Depending on the use case, you can also use async operators like .flatMap in combination with .subscribeOn or .publishOn to delegate specific blocking operations to another Scheduler, as described in the project reactor docs. For example:
flux
.flatMap(i -> Mono.fromCallable(() -> {
System.out.println("ReactorTests.test " + Thread.currentThread());
sleep(1000L); // simulate IO delay
return String.format("String %d", i);
})
.subscribeOn(Schedulers.boundedElastic()))
In fact, .flatMap also has an overloaded variant that takes a concurrency parameter where you can limit the maximum number of in-flight inner sequences. This can be used instead of .parallel in some use cases. It will not generally work for Flux.interval though, since Flux.interval doesn't support downstream requests that replenish slower than the ticks.
I will use Flux.range(1, 100) as the source of event in this demo;
change map() to flatMap() with a concurrency 3;
inside flatMap(), use Mono.fromCallable() to wrap your "IO delay code";
make the Mono.fromCallable to "subscribeOn" a Scheduler;
here is the code:
#Test
void testParallelEvent(){
System.out.println("Main thread: " + Thread.currentThread());
Flux<String> tick = ⚠️Flux.range(1, 100)
.⚠️flatMap(i-> ⚠️Mono.fromCallable(()->{
System.out.println("ReactorTests.test " + Thread.currentThread());
sleep(500L); // simulate IO delay
return String.format("String %d", i);
}).⚠️subscribeOn(Schedulers.boundedElastic())
, ⚠️3) // concurrency
.take(6);
// .subscribeOn(Schedulers.elastic());
// .subscribeOn(Schedulers.newParallel("test"));
// .subscribeOn(Schedulers.fromExecutor(executor));
tick.subscribe(x ->System.out.println("Subscribe thread: " + Thread.currentThread() + " --> " + x),
System.out::println,
()-> System.out.println("Done"));
System.out.println("DONE AND DONE");
sleep(8000);
}
and the output is:
Main thread: Thread[main,5,main]
16:35:34.034 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
ReactorTests.test Thread[boundedElastic-1,5,main]
ReactorTests.test Thread[boundedElastic-2,5,main]
DONE AND DONE
ReactorTests.test Thread[boundedElastic-3,5,main]
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 1
Subscribe thread: Thread[boundedElastic-3,5,main] --> String 3
ReactorTests.test Thread[boundedElastic-4,5,main]
ReactorTests.test Thread[boundedElastic-5,5,main]
Subscribe thread: Thread[boundedElastic-2,5,main] --> String 2
ReactorTests.test Thread[boundedElastic-3,5,main]
Subscribe thread: Thread[boundedElastic-4,5,main] --> String 4
ReactorTests.test Thread[boundedElastic-2,5,main]
Subscribe thread: Thread[boundedElastic-5,5,main] --> String 5
ReactorTests.test Thread[boundedElastic-4,5,main]
Subscribe thread: Thread[boundedElastic-3,5,main] --> String 6
Done
Exiting
Exiting
if you need to use Flux.interval() as the event source, you must add some backpressure strategy or otherwise end up with the OverflowException:
Flux.interval(Duration.ofMillis(10))
.onBackpressureBuffer(10) // backpressure strategy
.flatMap(...)
here is full source code with Flux.interval & onBackpressureBuffer:
#Test
void testParallelWithBackpressureBuffer(){
System.out.println("Main thread: " + Thread.currentThread());
Flux<String> tick = Flux.interval(Duration.ofMillis(10))
.onBackpressureBuffer(10) // ⚠️backpressure strategy
.flatMap(i-> Mono.fromCallable(()->{
System.out.println("simulate IO " + Thread.currentThread() + " " + i);
sleep(1000L); // simulate IO delay, very slow
return String.format("String %d", i);
}).subscribeOn(Schedulers.boundedElastic())
, 3)
.take(10);
Disposable disposable = tick.subscribe(x ->System.out.println("Subscribe thread: " + Thread.currentThread() + " --> " + x),
System.out::println,
()-> System.out.println("Done"));
while(!disposable.isDisposed()){
sleep(800);
System.out.println("..wait..");
}
System.out.println("DONE AND DONE");
}
and the result will be
Main thread: Thread[main,5,main]
15:08:52.854 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
simulate IO Thread[boundedElastic-1,5,main] 0
simulate IO Thread[boundedElastic-2,5,main] 1
simulate IO Thread[boundedElastic-3,5,main] 2
..wait..
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 0
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 1
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 2
simulate IO Thread[boundedElastic-4,5,main] 3
simulate IO Thread[boundedElastic-2,5,main] 4
simulate IO Thread[boundedElastic-3,5,main] 5
..wait..
Subscribe thread: Thread[boundedElastic-4,5,main] --> String 3
simulate IO Thread[boundedElastic-1,5,main] 6
Subscribe thread: Thread[boundedElastic-2,5,main] --> String 4
Subscribe thread: Thread[boundedElastic-2,5,main] --> String 5
simulate IO Thread[boundedElastic-3,5,main] 7
simulate IO Thread[boundedElastic-4,5,main] 8
..wait..
Subscribe thread: Thread[boundedElastic-1,5,main] --> String 6
simulate IO Thread[boundedElastic-2,5,main] 9
Subscribe thread: Thread[boundedElastic-3,5,main] --> String 7
Subscribe thread: Thread[boundedElastic-3,5,main] --> String 8
simulate IO Thread[boundedElastic-4,5,main] 10
simulate IO Thread[boundedElastic-1,5,main] 11
..wait..
..wait..
Subscribe thread: Thread[boundedElastic-2,5,main] --> String 9
Done
Exiting
Exiting
..wait..
DONE AND DONE

Why do loop iterations sometimes get shuffled when called in delay? [code & output]

I'm using swift 3, to make delayed events, this is the code
public func delay(bySeconds seconds: Double, dispatchLevel: DispatchLevel = .main, closure: #escaping () -> Void)
{
let dispatchTime = DispatchTime.now() + seconds
dispatchLevel.dispatchQueue.asyncAfter(deadline: dispatchTime, execute: closure)
}
public enum DispatchLevel
{
case main, userInteractive, userInitiated, utility, background
var dispatchQueue: DispatchQueue
{
switch self
{
case .main: return DispatchQueue.main
case .userInteractive: return DispatchQueue.global(qos: .userInteractive)
case .userInitiated: return DispatchQueue.global(qos: .userInitiated)
case .utility: return DispatchQueue.global(qos: .utility)
case .background: return DispatchQueue.global(qos: .background)
}
}
}
override func viewDidAppear(_ animated: Bool)
{
}
override func viewDidLoad()
{
super.viewDidLoad()
for i in 0..<20
{
delay(bySeconds: 1.0+Double(i)*0.5, dispatchLevel: .background)
{
print("__ \(i)")
// delayed code that will run on background thread
}
}
}
the actual output, notice the change of pattern after 10
__ 0
__ 1
__ 2
__ 3
__ 4
__ 5
__ 6
__ 7
__ 8
__ 9
__ 10
__ 12
__ 11
__ 14
__ 13
__ 16
__ 15
__ 17
__ 18
__ 19
the expected output
__ 0
__ 1
__ 2
__ 3
__ 4
__ 5
__ 6
__ 7
__ 8
__ 9
__ 10
__ 11
__ 12
__ 13
__ 14
__ 15
__ 16
__ 17
__ 18
__ 19
is there something wrong with the delay extension?
The key response is that you are using asynchronous function and concurent-queue(background) .
dispatchLevel.dispatchQueue.asyncAfter(deadline: dispatchTime, execute: closure)
The above code will retunrs immediately and it just set the task to be executed in a fututr time, as result your delay function will also return immediately. As result this does not block the loop from going to next (in contract with a sycn function).
Another side is the fact that background queue is a concurrent queue means no grantee that tasks will finish executing in the same order they are added the queue and this explain the resuts you get.
By contrast if you use main-queue as it is a serial-queue, then there is a garantee that it execute and finish one by one in the order they are added. but you are going to block the UI/ responsiveness of the app
You made the assumption that when task is scheduled first on concurrent queue, it will be executed first. A concurrent queue distribute its tasks over multiple threads, and even though they are added to each thread in order, each task will have a random delay that may cause them to execute out of order.
To illustrate the point, let's strip down your code to the bare minimum, and measure when a task was scheduled vs. when it was actually executed:
let queue = DispatchQueue.global(qos: .userInteractive)
for i in 0..<20 {
let scheduledTime = DispatchTime.now() + Double(i) * 0.5
queue.asyncAfter(deadline: scheduledTime) {
let threadID = pthread_mach_thread_np(pthread_self()) // The thread that the task is executed on
let executionTime = DispatchTime.now()
let delay = executionTime.uptimeNanoseconds - scheduledTime.uptimeNanoseconds
print(i, scheduledTime.uptimeNanoseconds, executionTime.uptimeNanoseconds, delay, threadID, separator: "\t")
}
}
// Wait for all the tasks to complete. This is not how you should wait
// but this is just sample code to illustrate a point.
sleep(15)
Result, in nanoseconds (some formatting added):
i scheduledTime executionTime delay threadID
0 142,803,882,452,582 142,803,883,273,138 820,556 3331
1 142,804,383,177,169 142,804,478,766,410 95,589,241 3331
2 142,804,883,221,388 142,804,958,658,498 75,437,110 3331
3 142,805,383,223,641 142,805,428,926,049 45,702,408 3331
4 142,805,883,224,792 142,806,066,279,866 183,055,074 3331
5 142,806,383,225,771 142,806,614,277,038 231,051,267 3331
6 142,806,883,229,494 142,807,145,347,839 262,118,345 3331
7 142,807,383,230,527 142,807,696,729,955 313,499,428 3331
8 142,807,883,231,420 142,808,249,459,465 366,228,045 3331
9 142,808,383,232,293 142,808,779,492,453 396,260,160 3331
10 142,808,883,233,183 142,809,374,609,495 491,376,312 3331
12 142,809,883,237,042 142,809,918,923,562 35,686,520 4355
11 142,809,383,234,072 142,809,918,923,592 535,689,520 3331
13 142,810,383,238,029 142,811,014,010,484 630,772,455 3331
14 142,810,883,238,910 142,811,014,040,582 130,801,672 4355
15 142,811,383,239,808 142,812,119,998,576 736,758,768 4355
16 142,811,883,240,686 142,812,120,019,559 236,778,873 3331
18 142,812,883,242,410 142,813,228,621,306 345,378,896 4355
17 142,812,383,241,550 142,813,228,646,734 845,405,184 3331
19 142,813,383,245,491 142,814,307,199,255 923,953,764 3331
The 20 tasks were distributed across 2 threads (3331 and 4355). Within each thread, the tasks were executed in order but the threads may be sent to a different CPU cores and hence causes out-of-order execution. Each task is also randomly delayed by up to 900ms. That's the trade-off in using queues: you have no control over the delay since who know what else is running on these global queues. You have 3 options here:
If timing is super-critical and you want to get a task executed with as little delay as possible, create and manage your own thread.
Use a serial instead of concurrent queue.
Scheduling dependent tasks by introducing a delay is usually a bad idea anyhow. Look into NSOperationQueue which allows you to specify the dependencies between tasks.

iPad Cordova app sometimes crashes on launch

My iPad app crashes on launch. It is reported only by some users.
Reinstalling the app seems to make it work.
Exception Type: 00000020
Exception Codes: 0x000000008badf00d
Highlighted Thread: 3
Application Specific Information:
<BKNewProcess: 0x14c5303c0; com.xxx.xxx; pid: 478; hostpid: -1> has active assertions beyond permitted time:
{(
<BKProcessAssertion: 0x14c6185c0> id: 478-1FE86162-9E03-459B-B266-BCEF0B247A17 name: Called by XXXX, from -[AppDelegate applicationDidEnterBackground:] process: <BKNewProcess: 0x14c5303c0; com.xxx.xxx; pid: 478; hostpid: -1> permittedBackgroundDuration: 180.000000 reason: finishTask owner pid:478 preventSuspend preventIdleSleep preventSuspendOnSleep
)}
Elapsed total CPU time (seconds): 380.760 (user 380.760, system 0.000), 98% CPU
Elapsed application CPU time (seconds): 0.061, 0% CPU
My background task code is
- (void)applicationDidEnterBackground:(UIApplication *)application
{
/*
Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
*/
NSLog(#"Background time remaining in didEnterBackground:%f",[[UIApplication sharedApplication] backgroundTimeRemaining]);
__block UIBackgroundTaskIdentifier backgroundTask;
backgroundTask = [application beginBackgroundTaskWithExpirationHandler: ^ {
[application endBackgroundTask: backgroundTask];
backgroundTask = UIBackgroundTaskInvalid;
NSLog(#"Background task Completed.");
}];
}
thread 3:
Thread 3 name: com.apple.NSURLConnectionLoader
Thread 3:
0 libsystem_kernel.dylib 0x389814f0 0x38980000 + 5360
1 libsystem_kernel.dylib 0x389812e5 0x38980000 + 4837
2 CoreFoundation 0x2a750317 0x2a684000 + 836375
3 CoreFoundation 0x2a74e8bd 0x2a684000 + 829629
4 CoreFoundation 0x2a69c3bd 0x2a684000 + 99261
5 CoreFoundation 0x2a69c1cf 0x2a684000 + 98767
6 CFNetwork 0x2a251953 0x2a1d5000 + 510291
7 Foundation 0x2b49ab57 0x2b3ca000 + 854871
8 libsystem_pthread.dylib 0x38a11e91 0x38a0f000 + 11921
9 libsystem_pthread.dylib 0x38a11e03 0x38a0f000 + 11779
10 libsystem_pthread.dylib 0x38a0fb8c 0x38a0f000 + 2956
Based on the Xcode documentation, i see that this exception type means that the watchdog is terminating the app. I'm not able to reproduce the crash and do not have the device console logs.
Please help to analyze this crash log.

Resources