RunLoop.main or DispatchQueue.main when using Combine scheduler?

whereas the DispatchQueue variant will perhaps execute immediately if optimizations in libdispatch kick in.

@Philippe_Hausler Is this something specific to combine? I'm not seeing this ever come into effect when using DispatchQueue.main.async

Nope, that is a generalized optimization that is applicable to libdispatch. The main queue has slightly specialized behavior in that regards. This optimization is one of the reasons why crossing the streams for threads and queues is problematic with things like thread locals etc.

So I created a new single view application in xcode 10.2, and made the following change:

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
      
      print("Before")
      DispatchQueue.main.async {
        print("During1")
        
        DispatchQueue.main.async {
          print("During2")
        }
        print("During3")
      }
      print("After")
    }

With the result of:

Before
After
During1
During3
During2

If that optimization was taking place, I would expect:

Before
During1
During2
During3
After

Is there more to it?

Yea it is a bit more intricate than that. The optimization is a bit easier to see on non-main-queues. The one way to see it on main is when dispatchMain or xpc_main are invoked; those actually exit the main pthread but leave a main queue.

So I have a conjecture on why the 'serial' vs 'concurrent' issue isn't enforced.
I had a similar issue crop up when I built FutureKit.

There isn't anything 'illegal' about dispatching into a concurrent DispatchQueue. The problem is that any 'later' blocks are no longer guaranteed to arrive in the same order..

For example:

    methodReturningPublisher()
       .receive(on: concurrentQueue)
       .map { "\($0)" }
       .sink { value in print(value) }

It's possible for two events to be sent from the publisher, but for them to printed out-of-order. It could even be that each 'print' operation in the sink could be executing in different threads, meaning the 'sink' code better be thread-safe.

I can imagine a case where the 'sink' is just performing some work on a queue of work items, and there is a performance improvement allowing the sink to run concurrent/multithreaded, and you don't care if sink fires 'out of order'. But it's a rare case for most Publishers, and a definite no-no for publishers used to bind to Views.

2 Likes

Hey @mishagray - it's definitely not enforced, I've got some tests and trials that don't illuminate any difference, and the specifics here aren't enforced (or potentially enforceable?) by the compiler

Sorry to resurrect an old thread, but I want to point out a problem with the following claim:

There's a big difference: RunLoop, as a Scheduler, schedules actions to run only in the default mode. So, for example, if you're using Combine to load images in the background, and then you receive(on: RunLoop.main) to stuff them into table view cells, you're going to discover that your images don't appear while the user is scrolling the table view.

The main RunLoop drains the main DispatchQueue in all modes the .common modes (thanks @eskimo), which include the tracking modes. So a Combine pipeline that uses receive(on: DispatchQueue.main) will continue to emit signals during user interactions.

(Confidential to Apple-jacks: FB7656836)

22 Likes

The main RunLoop drains the main DispatchQueue in all modes.

Not quite. It actually drains the main queue in the common modes.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

8 Likes

Does this mean that I can actually loose the latest value because the .finished completion event arrives before the last value event?

Thats what I currently see in a small sample with

let cancel = 1...3
  .publisher
  .receive(on: privateQueue)
  .map({ print("map \($0)"); sleep($0); return $0})
  .receive(on: ...main)
  .sink {...}

which in turn means that concurrent queues cannot be used in this way, right?
The print in map is executed, but the sinks receiveValue is not.

Related but not related.

I was hoping that .receive(on: DispatchQueue.main) would deliver synchronously if the notification was sent from the main thread and asynchronously otherwise. Instead I think I'm seeing* that it always delivers asynchronously to the main thread — even if the original notification was from the main thread.

The consistency might be a good thing, but it means that if performance matters I have to know which threads both the publisher and subscriber are on. What I was hoping was that I could make a declaration of my intent and have Combine magically optimize for it.

===

*Looking at the call stack, async delivery definitely seems to be the case for .receive(on: RunLoop.main). For .receive(on: DispatchQueue.main) it's less clear: the original publisher notification is still on the call stack, but so are calls like dispatch_async. Maybe .receive(on: DispatchQueue.main) is something of a hybrid, ultimately making a sync call from an async request? (When I don't use .receive() at all the call stack is of course much simpler.)

1 Like

has this feedback been solved? can't find anything about it.

RunLoop.main and DispatchQueue.main are almost identical.

There is one key difference here though.

RunLoops can be busy. And if that happens, your publisher will wait until it can publish the change on the RunLoop.

DispatchQueues on the other hand will publish regardless.

A good example is, make a list of images that need to be downloaded from the web.

Try scrolling while the image is downloading.

With the RunLoop, the images won’t appear until you finish scrolling.

With the dispatch queue, they will.

Here’s an article with a good explanation: https://www.avanderlee.com/combine/runloop-main-vs-dispatchqueue-main/

Try scrolling while the image is downloading.

That deserves further explanation. The difference between these cases is that you’re scheduling the run loop source in the default mode whereas Dispatch services the main queue is any common mode. If you schedule your run loop source in the common modes, you’ll see the same behaviour.

This is a common (ahem) source of confusion. My favourite explanation of this (because I wrote it, natch! :-) is in WWDC 2010 Session 208 Network Apps for iPhone OS, Part 2. Sadly, that presentation is no longer available from Apple, so I posted a transcript of the relevant section to DevForums.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

7 Likes

BTW, I was looking for a specific no-longer-available wwdc video before and found it on an obscure Apple hosted place. Perhaps all videos are still in there, it's just a matter of knowing the link ;-)