1
import Dispatch
import Foundation

DispatchQueue.main.async {
    print("just print in main async")
}

RunLoop.current.run(mode: .default, before: .distantFuture)

print("RunLoop.current.run ends!")

enter image description here

If the runLoop ends after printing "just print in main async", does it means that the runLoop was terminated by main.async (just like an UI event)?

I want to know what exactly happens in this case.

eczn
  • 1,459
  • 2
  • 10
  • 15
  • 1
    The documentation of [`DispatchQueue.main`](https://developer.apple.com/documentation/dispatch/dispatchqueue/1781006-main) says that if you use a run loop on the main thread, the tasks submitted to the main queue will run. Perhaps that could imply that the main queue is an input source. – Sweeper Jul 20 '23 at 09:32

2 Answers2

3

You asked:

Why can DispatchQueue.main.async be used as an input source for RunLoop.current.run?

The behavior you described does not necessarily mean that DispatchQueue.main.async {…} is, technically, a runloop “input source”. It only means that the run(mode:before:) can allow libDispatch to run dispatched items before returning. We do not have access to these implementation details.

If the runLoop ends after printing “just print in main async”, does it [mean that the runloop] was terminated by main.async (just like an UI event)?

Not necessarily. A few observations:

  • Not to split hairs, but it is not “terminated” when you return from run(before:mode:), but rather that it just runs the loop once. It is simply a question of whether it runs the dispatched blocks before returning or not.

  • I see slightly different behavior in macOS and iOS targets and where I do this (and whether I did this in an @IBAction or viewDidLoad or what have you). This makes me even more hesitant to make material assumptions about the implementation details.

  • I am seeing race-like behaviors. Consider the following (where I replaced the print with “Points of Interest” signposts):

    import UIKit
    import os.log
    
    let poi = OSLog(subsystem: "Test", category: .pointsOfInterest)
    
    class ViewController: UIViewController {
        @IBAction func didTapButton(_ sender: Any) {
            for i in 0 ..< 10 {
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    print("after")
                    os_signpost(.event, log: poi, name: "After 1 sec", "%d", i)
                }
            }
    
            for i in 0 ..< 10 {
                DispatchQueue.main.async {
                    print("immediate", i)
                    os_signpost(.event, log: poi, name: "Immediate", "%d", i)
                }
            }
    
            RunLoop.current.run(mode: .default, before: .distantFuture)
    
            os_signpost(.event, log: poi, name: "Run")
        }
    }
    

    Sometimes I got:

    enter image description here

    Other times I got:

    enter image description here

    The former behavior is admittedly hard to reproduce. But races are often hard to manifest.

My bottom line is that I would hesitate to use the term “runloop input source” in conjunction with items dispatched to a queue (without seeing RunLoop source code or some formal assurances from Apple). GCD and runloops are very different tech stacks/patterns. We would generally use GCD (or Swift concurrency or what have you) in lieu of runloop patterns. Furthermore, I would hesitate to rely on a RunLoop.current.run to drain the queued items from a dispatch queue.

GCD largely eliminates the need for the legacy runloop run (anti)patterns. If the question was purely out of curiosity, then hopefully the above offers some useful observations. But if the runloop is being introduced to achieve some functional behavior in GCD, I might suggest posting a separate question on that broader question and avoid introducing RunLoop into the mix.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
-2

Yes, I think in the provided code, the run loop will end after printing "just print in main async" because of the DispatchQueue.main.async block.

As run loop will simply wait for events indefinitely (until an event occurs, because you have provided .distantFuture).

At some point, an event occurs, which can be a user interaction event (UI event) or another source getting processed. This event will interrupt the run loop and allow the scheduled DispatchQueue.main.async block to execute on the main queue.

Then "just print in main async" will be printed from the asynchronously executed block.

However, after the printed statement there are no more events scheduled, so the run loop will eventually exit and print "RunLoop.current.run ends!".

And hope this link helps, too.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
jullyJ
  • 1
  • 2