← Назад к вопросам
Что происходит с потоком в RunLoop, когда нет входящих событий?
2.0 Middle🔥 141 комментариев
#Многопоточность и асинхронность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Что происходит с потоком в RunLoop, когда нет входящих событий?
Это классический вопрос о внутреннем устройстве RunLoop. Когда нет событий, поток не крутится в плотном цикле, а переходит в sleep режим, экономя батарею и CPU.
Цикл жизни RunLoop
RunLoop постоянно крутится, выполняя циклических обработку:
while (running) {
// 1. Проверяем входящие события
while (hasEvents()) {
processEvent()
}
// 2. Если нет событий — засыпаем
sleep() // поток блокируется в kernel space
// 3. Когда событие приходит — просыпаемся
// (это может быть touch, timer, network callback и т.д.)
}
Подробное объяснение
Фаза 1: Активная обработка событий
Когда есть события (touch, callbacks, timers):
let runLoop = RunLoop.main
// RunLoop обрабатывает события для каждого mode
while !runLoop.exited {
runLoop.run(until: Date(timeIntervalSinceNow: 0.01)) // 10ms
}
Рунлуп идёт через все зарегистрированные обработчики (sources, timers, observers).
Фаза 2: Sleep режим (когда нет событий)
Когда очередь событий пуста:
Thread State: SLEEPING
├─ CPU: 0% (не потребляет)
├─ Memory: Выделена, но неиспользуется
├─ Status: Ждёт в kernel'е
└─ Wake-up trigger: событие из kernel
Поток блокируется на системном уровне (kernel sleep), ожидая события:
// CFRunLoopRunSpecific использует mach_msg
// Это системный вызов, который блокирует поток
// Поток переходит из user space в kernel space
kernelspace {
mach_msg_receive() // блокирующий вызов
// Поток ждёт сообщения от kernel'а
// Когда сообщение приходит — просыпается
}
Реальный пример
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Главный поток крутится в RunLoop.main
// Когда сейчас ничего не происходит:
// ✓ Поток СПИТ в kernel'е
// ✓ CPU не потребляется
// ✓ Ждёт события (touch, timer, network callback)
// Когда пользователь трогает экран:
// 1. Kernel получает interrupt
// 2. Kernel пробуждает поток
// 3. RunLoop обрабатывает touch event
// 4. Если событий больше нет — снова спит
}
@IBAction func buttonTapped(_ sender: UIButton) {
// Поток проснулся, обрабатывает action
// Когда закончит — снова спит
}
}
Внутреннее устройство: mach_msg
Рунлуп использует mach kernel IPC для sleep/wake:
// Упрощённо, вот что происходит
func runLoop() {
while true {
// Обработка текущих событий
for event in currentEvents {
processEvent(event)
}
// Нет событий? Спим
if eventQueue.isEmpty {
// mach_msg_receive — блокирующий системный вызов
// Поток переходит в sleep
let status = mach_msg_receive(
receiveBuffer: eventQueue,
timeout: TIMEOUT_FOREVER // или с timeout
)
// Kernel'ь пробудит нас когда придёт событие
// Или timeout истечёт
}
}
}
Что может разбудить поток
// 1. Touch event
touchBegan(_ touch: UITouch) { } // поток просыпается
// 2. Timer
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
// Поток просыпается через 1 сек
}
// 3. Network callback
URLSession.shared.dataTask(with: url) { data, response, error in
// Поток просыпается когда приходит ответ
}.resume()
// 4. GCD async
DispatchQueue.main.async {
// Поток просыпается из DISPATCH_QUEUE_SERIAL
}
// 5. NSPort message
port.send(data) // разбудит RunLoop, ждущий этого port'а
// 6. Вручную
RunLoop.main.wakeUp() // явно разбудить RunLoop
Режимы RunLoop
Разные режимы имеют разные источники событий:
// Default mode
RunLoop.main.run() // обрабатывает
// ├─ Touch events
// ├─ NSTimer (если добавлен в default)
// └─ Custom sources
// Tracking mode (во время скролла)
RunLoop.main.run() // обрабатывает
// ├─ Touch tracking events
// └─ Timers (если добавлены в tracking)
// UITrackingRunLoopMode vs CommonModes
var timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
print("Timer") // СТОПИТСЯ во время скролла!
}
// Добавляем в CommonModes
RunLoop.main.add(timer, forMode: .commonModes) // работает везде
Power Efficiency
Спящий в RunLoop поток очень эффективен:
Деятельный поток: ████████████████ 100% CPU
Спящий в RunLoop: ░░░░░░░░░░░░░░░░ 0% CPU
Усиленный цикл: ████████████████ 100% CPU
// ❌ ПЛОХО: плотный цикл, 100% CPU
var running = true
while running {
// ❌ это сжирает батарею!
// поток постоянно работает
processEvents() // но нечего обрабатывать
// результат: горячий телефон, разряженная батарея
}
// ✅ ХОРОШО: RunLoop с sleep
RunLoop.main.run() // поток спит, когда нечего делать
// результат: холодный телефон, долгая батарея
Практические следствия
1. Timers иногда не срабатывают
var timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
print("Tick") // может не срабатывать во время скролла
}
// Решение: добавь в CommonModes
RunLoop.main.add(timer, forMode: .commonModes)
2. GCD callback может задержаться
DispatchQueue.main.async {
print("Hello") // выполнится когда RunLoop проснётся
}
// Если в главном потоке ничего не происходит:
// 1. GCD callback встанет в очередь
// 2. RunLoop проснётся
// 3. Callback выполнится
3. Очень высокая нагрузка может заморозить UI
func processHugeArray(_ arr: [Int]) {
// ❌ ПЛОХО: блокирует главный поток
for item in arr { // миллионы итераций
heavyProcessing(item) // RunLoop не крутится!
// UI не может отреагировать на touch
}
}
// ✅ ХОРОШО: используй background queue
DispatchQueue.global().async {
for item in arr {
heavyProcessing(item) // главный поток спит
}
DispatchQueue.main.async {
// обновляем UI на главном потоке
updateUI()
}
}
Вывод
Когда в RunLoop нет событий:
- Поток СПИТ на уровне kernel'я (блокирующий системный вызов mach_msg_receive)
- CPU = 0% — батарея не потребляется
- Ждёт события — touch, timer, network callback, GCD async
- Просыпается когда kernel сигнализирует о событии
- Обрабатывает событие, затем снова спит
Эта архитектура обеспечивает энергоэффективность и отзывчивость UI на iOS.