← Назад к вопросам

Что происходит с потоком в 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 нет событий:

  1. Поток СПИТ на уровне kernel'я (блокирующий системный вызов mach_msg_receive)
  2. CPU = 0% — батарея не потребляется
  3. Ждёт события — touch, timer, network callback, GCD async
  4. Просыпается когда kernel сигнализирует о событии
  5. Обрабатывает событие, затем снова спит

Эта архитектура обеспечивает энергоэффективность и отзывчивость UI на iOS.

Что происходит с потоком в RunLoop, когда нет входящих событий? | PrepBro