dispatch_after в Swift: GCD под капотом — разбор на примерах

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

dispatch_after в Swift: как GCD выполняет отложенные задачи под капотом

Представьте: вы пишете приложение на Swift, и вам нужно выполнить кусок кода не сразу, а, скажем, через 3 секунды. Первое, что приходит в голову — dispatch_after. Но если вы заглянете в старые версии Swift или на Stack Overflow, то увидите, что этот метод изменился. А главное — мало кто понимает, что на самом деле происходит внутри Grand Central Dispatch (GCD), когда мы говорим: «Сделай это через 2 секунды».

В этой статье мы не просто покажем, как писать dispatch_after в современном Swift. Мы залезем под капот GCD, разберём очереди, потоки и таймеры, чтобы вы могли писать надёжный и эффективный асинхронный код. Поехали!

Что такое dispatch_after и почему он стал asyncAfter?

В старых версиях Swift (1.x, 2.x) вы могли встретить такой код:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2 * NSEC_PER_SEC)), dispatch_get_main_queue()) {
    print("Прошло 2 секунды")
}

Этот код работал, но выглядел громоздко. Начиная с Swift 3, Apple провела рефакторинг всего GCD API, сделав его более «свифтовым». Теперь тот же самый функционал выглядит так:

DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
    print("Прошло 2 секунды")
}

Обратите внимание: asyncAfter — это метод очереди (DispatchQueue), а не отдельная функция. Это ключевое изменение: теперь вы явно указываете, на какой очереди выполнять задачу.

Как GCD работает под капотом: очереди, потоки и таймеры

Чтобы понять dispatch_after (или asyncAfter), нужно разобраться в трёх сущностях:

  • DispatchQueue — очередь задач. Может быть последовательной (serial) или параллельной (concurrent).
  • DispatchWorkItem — задача, которую вы ставите в очередь. В нашем случае это замыкание.
  • DispatchTime — момент времени, когда задача должна запуститься. GCD использует системный таймер для отслеживания дедлайнов.

Когда вы вызываете asyncAfter, GCD не создаёт новый поток. Вместо этого он:

  1. Создаёт таймер, который сработает в указанное время.
  2. Помещает задачу в специальную очередь отложенных задач (часто её называют dispatch timer).
  3. Когда таймер срабатывает, GCD перемещает задачу в обычную очередь DispatchQueue, где она ждёт своего потока.

Если очередь, в которую вы ставите задачу, занята другими задачами, ваша отложенная задача может выполниться позже указанного дедлайна. Это важный момент: dispatch_after гарантирует, что задача не выполнится раньше дедлайна, но может выполниться позже.

Практический пример: dispatch_after для обновления UI

Самый частый сценарий — обновление интерфейса через некоторое время после действия пользователя. Допустим, вы показываете сообщение «Сохранено», а через 2 секунды убираете его:

func showTemporaryMessage() {
    let messageLabel = UILabel()
    messageLabel.text = "Сохранено!"
    messageLabel.alpha = 1.0
    view.addSubview(messageLabel)

    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
        UIView.animate(withDuration: 0.5) {
            messageLabel.alpha = 0.0
        } completion: { _ in
            messageLabel.removeFromSuperview()
        }
    }
}

Обратите внимание: мы используем DispatchQueue.main, потому что все операции с UI должны выполняться на главном потоке. Если бы мы вызвали asyncAfter на фоновой очереди, а потом попытались изменить UI, приложение бы упало.

Подводные камни dispatch_after: утечки памяти и захват self

Одна из самых частых ошибок — сильная ссылка на self внутри замыкания. Представьте, что вы пишете:

class ViewController: UIViewController {
    func scheduleTask() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            self.updateUI() // self захвачен сильно
        }
    }
}

Если пользователь уйдёт с экрана до того, как пройдёт 5 секунд, self (ViewController) не будет освобождён, потому что GCD держит на него сильную ссылку. Это приводит к утечке памяти.

Решение — использовать слабый захват:

DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
    guard let self = self else { return }
    self.updateUI()
}

Или, если вы используете Swift 5.5+ и iOS 13+, можно написать ещё короче:

DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
    self?.updateUI()
}

Отмена отложенной задачи: DispatchWorkItem

Иногда нужно отменить задачу, которая ещё не выполнилась. Например, пользователь нажал кнопку «Отмена». Для этого используйте DispatchWorkItem:

var pendingTask: DispatchWorkItem?

func scheduleCancellableTask() {
    let task = DispatchWorkItem { [weak self] in
        self?.doSomething()
    }
    pendingTask = task
    DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: task)
}

func cancelTask() {
    pendingTask?.cancel()
    pendingTask = nil
}

Важно: cancel() не останавливает задачу, если она уже выполняется. Он только устанавливает флаг isCancelled. Внутри задачи вы должны сами проверять этот флаг:

let task = DispatchWorkItem {
    guard !Task.current.isCancelled else { return }
    // выполняем работу
}

dispatch_after и точность времени: когда «через 2 секунды» — это не ровно 2 секунды

GCD использует системный таймер, который имеет ограниченную точность. Обычно это около 1 миллисекунды, но если система сильно загружена, задержка может быть больше. Есть два типа дедлайнов:

  • DispatchTime — абсолютное время. Задача выполнится как можно ближе к этому моменту.
  • DispatchWallTime — привязано к настенным часам. Используется, когда важна точность относительно реального времени (например, будильник).
// DispatchTime — стандартный подход
let deadline = DispatchTime.now() + 2.0

// DispatchWallTime — точное время с учётом сна устройства
let wallDeadline = DispatchWallTime.now() + 2.0
DispatchQueue.global().asyncAfter(wallDeadline: wallDeadline) {
    print("Это сработает через 2 секунды даже после сна")
}

Разница: если устройство уходит в сон, DispatchTime «заморозится» вместе с ним, а

Блоги

Рекомендаций по книгам