What is a run loop, and what happens in one iteration?
Predict first
In what order do the observer activities and the main-queue block appear for one turn of the loop?
import Foundation
import CoreFoundation
var log: [String] = []
let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.allActivities.rawValue, true, 0) { _, activity in
switch activity {
case .entry: log.append("entry")
case .beforeTimers: log.append("beforeTimers")
case .beforeSources: log.append("beforeSources")
case .beforeWaiting: log.append("beforeWaiting")
case .afterWaiting: log.append("afterWaiting")
case .exit: log.append("exit")
default: break
}
}
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, .defaultMode)
DispatchQueue.main.async { log.append("MAIN-ASYNC") }
log.append("before-run")
RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 0.2))
log.append("after-run")
print(log.joined(separator: " "))
- before-run entry MAIN-ASYNC exit after-run
- before-run entry beforeTimers beforeSources MAIN-ASYNC exit after-run
- MAIN-ASYNC before-run entry exit after-run
Predict first
What does this print?
import Foundation
var order: [String] = []
DispatchQueue.main.async { order.append("async") }
order.append("sync-after-schedule")
RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 0.05))
print(order)
- ["async", "sync-after-schedule"]
- ["sync-after-schedule", "async"]
Predict first
What mode is a plain background thread’s run loop running in?
import Foundation
let t = Thread { print(RunLoop.current.currentMode as Any) }
t.start()
Thread.sleep(forTimeInterval: 0.1)
- default
- nil, no loop is running
- common
Mental model. A waiter making rounds: check the timers on the wall, see if any orders came in, serve them, then sit down until the bell rings.
A run loop is an event loop attached to a thread. Its job: block cheaply until there is work, process it, block again. Inside it, work comes from sources (input such as touches, ports and sockets), timers, and the loop’s own observers, which are callbacks at fixed points. Each iteration notifies observers (before timers, before sources, before waiting), fires whatever is due, then sleeps on a kernel port (after waiting when it wakes) using no CPU. The main thread’s loop is started by UIApplicationMain; it drives touch delivery, timers, the main dispatch queue and the commit of UI changes. A background thread has no running loop unless you start one.
Two practical facts follow. Work you schedule on the main queue does not run immediately: it runs when the loop next gets to its sources, after the current code returns. And long work on the main thread stops the loop from turning, so no touches, timers or drawing happen until it finishes. The loop also manages an autorelease pool per iteration, which is why temporary Objective-C objects are released regularly in app code.
The loop notifies before timers and before sources, then handles the source that carries the main-queue block, and exits after that one pass.
Scheduling does not run the block. It runs when the loop turns, after the current code has finished.
A secondary thread has no running loop until you run one, so currentMode is nil.
Common traps
- Expecting
DispatchQueue.main.asyncto run before the next line. It runs after the current call stack unwinds and the loop turns. - Assuming a
Timerworks on any thread. It needs a running loop on the thread it was scheduled on.
They might follow up with
- Which sources are “source0” and “source1”? Source0 is signalled by your app and needs the loop to be woken; source1 is driven by kernel port messages and wakes the loop itself.
- What does
run(mode:before:)do? It processes work once and returns;run()andrun(until:)keep looping.
Related questions
- 6.3.2 What are run loop modes, and why does a Timer stop firing while the user scrolls?
- 6.3.3 What is the main-thread contract, and why does DispatchQueue.main.sync from the main thread crash?
- 3.1.5 What is an autorelease pool, and when does a Swift loop need one?
What are run loop modes, and why does a Timer stop firing while the… →