简体   繁体   中英

Recursively sending values to combineLatest publisher

I came across some unexpected behavior in Combine that i'm hoping someone may be able to explain. I would expect the following code to create an infinite loop, but it actually only runs through the stream once.

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()

pub1
    .handleEvents(receiveOutput: { print("Received new pub1 value: \($0)") })
    .combineLatest(pub2)
    .handleEvents(receiveOutput: { print("Received new combined value: \($0)") })
    .sink { value in
        print(value)
        pub1.send(value.0)
    }.store(in: &subscriptions)

print("sending 1")
pub1.send(1)
print("sending 2")
pub2.send(2)

Generates the following output:

Received new pub1 value: 1
sending 2
Received new combined value: (1, 2)
(1, 2)
Received new pub1 value: 1

Since the value inside pub1 feeds back into itself I would expect sink to be called over and over. What's interesting is that if I get rid of the combineLatest, then this code will create an infinite loop. Something about the combineLatest operator is preventing it and I have no idea what.

I also noticed that adding .receive(on: DispatchQueue.main) before or after the combineLatest will also trigger a loop. I guess I'm not understanding something about how threads are handled in Combine. I'm not seeing this non-looping behavior with other operators. For instance merge(with:) will also create a loop, as expected.

The behaviour you noticed is caused by an implementation detail of the CombineLatest operator. Doing some low level debugging within the binary of the Combine framework revealed this particular instructions:

拆卸

Line 95 is where things diverge, when making the pub1.send(1) and pub2.send(2) calls, the execution continues with the next instructions (line 96), while the send() call from within the sink closure passes the jne test, and the execution jumps towards the end of the function.

The call stack looks like this:

在此处输入图像描述

Seems that the CombineLatest implementation either has some safeguard against recursiveness, or this is a side-effect of another implementation detail.

Note that the memory addresses and exact instruction might depend on the computer where the executable was built, however the idea remains the same.

It's just because what you are doing is not asynchronous: the sink send command is still taking place at the time that the value it sends is trying to come down the pipeline:

   pub1   <- - -
      |        |
    pub1.send -

If you permit the poor old send command to have a little temporal freedom, you'll get your infinite loop:

    DispatchQueue.main.asyncAfter(deadline: .now())  {
        pub1.send(value.0)
    }

(That also explains your success using receiveOn . It's the same trick.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM