How to use Combine Timer.pubish(…) in Command Line Tool?

I'm trying Combine, RxKotlin and RxJS/TypeScript.

To do that with Swift I used a Command Line Tool App to try / show experimentation of Publishers, Subjects, Operators & Subscribers.
For Kotlin I do that with a simple IntelliJ/Kotlin project.

While experiencing "Timer" I encountered the problem it doesn't work on Command Line Tool (CLT). I searched to figure out why. I understand the command line tool delegate to the command processor (Bash, Zsh, Sh...) to run the loop.

So I read here and there I had to use "RunLoop" to establish a Loop inside the CLT.

I wrote that that work.

let timer = Timer(timeInterval: 1, repeats: true) {_ in
        print("-")
}
RunLoop.current.add(timer, forMode: .common)
RunLoop.current.run()

But how can I simply experience this below?

Timer.publish(every: 1.0, on: .main, in: .default)
    .autoconnect()
    .sink { _ in
        print("timer fired")
    }
1 Like

The publisher will add the timer to the run loop when you call connect() or, as in your case, subscribe with autoconnect(). But you still have to run the run loop manually. This works:

import Combine
import Foundation

let subscription = Timer.publish(every: 1.0, on: .main, in: .default)
  .autoconnect()
  .sink { _ in
    print("timer fired")
  }

withExtendedLifetime(subscription) {
  RunLoop.current.run()
}

I believe the withExtendedLifetime dance is necessary to ensure the subscription is still alive when you run the run loop. Without it, the compiler is free to destroy the subscription immediately.

(The code did work without this in my brief tests, even when compiled with -O, but a different compiler version could choose to reorder when subscription is released.)

1 Like

Thanks Ole. Run fine.
I tried to put the RunLoop.current.run() alone, just after the subscription, like it looks to be "natural" but it didn't work.

You say this worked without the need of "withExtendedLifetime()" or even without the RunLoop control?

If I understand you on compilation, it could be an option that restrain the compiler to not destroy some instances to quickly?

It did work for me without withExtendedLifetime, but as I said, you can't rely on it. Subtle changes in how you write the code or in what context it's in may change how the compiler optimizes it.

Here's my test:

$ cat runloop-timer.swift
import Combine
import Foundation

let subscription = Timer.publish(every: 1.0, on: .main, in: .default)
  .autoconnect()
  .sink { _ in
    print("timer fired")
  }

RunLoop.current.run()

$ swift --version
Apple Swift version 5.3 (swiftlang-1200.0.29.2 clang-1200.0.30.1)
Target: x86_64-apple-darwin19.6.0

$ swiftc -O runloop-timer.swift
$ ./runloop-timer
timer fired
timer fired
timer fired
^C

(Note that it doesn't work if I omit the subscription variable altogether and ignore the "result of call to 'sink(receiveValue:)' is unused" compiler warning.)

The compiler could certainly be implemented in such a way to never destroy objects before they go out of scope, but it was a deliberate decision to be more aggressive because doing so enables certain important optimizations.

@lukasa gives an example of this toward the end of this excellent post on the same topic: An unexpected deadlock with Combine only on Release build - #6 by lukasa

You might well ask: well then, why not make release match debug? Why not make the lifetime of all references explicitly end when their enclosing scope ends?

The easiest answer to this is that it makes Swift programs slower. Consider this code:

Thanks. I'll read that.

Really impressive. I didn't work at all without "withExtendedLifetime". I had to quit Xcode and restart my machin then now it works without.

I tried on different app templates. Same.