Background thread в Swift: гайд для начинающих и профи

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

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

Начать курс

Почему фоновые потоки — это боль каждого iOS-разработчика?

Представьте: вы пишете приложение для загрузки фотографий из интернета. Всё работает идеально... пока пользователь не нажимает кнопку "Загрузить". Экран зависает, приложение "тормозит", а через пару секунд iOS просто убивает ваш процесс. Знакомо?

Проблема в том, что все операции по умолчанию выполняются на главном потоке (main thread). А главный поток отвечает за отрисовку UI — если вы его заблокируете долгой задачей, интерфейс перестанет реагировать на касания. Решение? Использовать background thread (фоновый поток) в Swift.

В этой статье мы разберёмся, как работать с фоновыми потоками на реальных примерах. Вы узнаете про GCD, OperationQueue и новый async/await из Swift 5.5+. Поехали!

Что такое поток (thread) в Swift?

Поток — это лёгкий процесс, который выполняет код программы. У iOS-приложения есть главный поток (main thread) и множество фоновых. Главный поток занимается UI: обновляет экран, обрабатывает жесты, анимации. Всё, что касается интерфейса, должно происходить только на главном потоке.

Фоновые потоки (background threads) нужны для:

  • Загрузки данных из сети
  • Работы с файлами (чтение/запись)
  • Сложных вычислений (обработка изображений, шифрование)
  • Работы с базами данных

Если вы попытаетесь обновить UI из фонового потока — приложение упадёт с ошибкой, либо вы увидите странное поведение интерфейса.

GCD (Grand Central Dispatch) — классический способ

Grand Central Dispatch (GCD) — это низкоуровневая библиотека для управления потоками, встроенная в iOS/macOS. Она работает через очереди (queues): вы помещаете задачу в очередь, а система сама решает, на каком потоке её выполнить.

DispatchQueue.main — главный поток

Если вам нужно выполнить код на главном потоке (например, обновить UILabel), используйте:

DispatchQueue.main.async {
    self.label.text = "Загрузка завершена!"
}

DispatchQueue.global() — фоновый поток

Для фоновых задач используйте глобальную очередь:

DispatchQueue.global(qos: .background).async {
    // Тяжёлая работа
    let imageData = try? Data(contentsOf: url)
    
    // Возвращаемся на главный поток для UI
    DispatchQueue.main.async {
        self.imageView.image = UIImage(data: imageData!)
    }
}

Параметр qos (Quality of Service) определяет приоритет задачи:

  • .userInteractive — самый высокий, для UI
  • .userInitiated — пользователь ждёт результат
  • .utility — долгие задачи (загрузка файлов)
  • .background — низкий приоритет (синхронизация, бэкапы)

OperationQueue — больше контроля

OperationQueue — это надстройка над GCD с дополнительными возможностями:

  • Отмена задач
  • Зависимости между задачами
  • Максимальное количество параллельных потоков

Пример загрузки нескольких изображений:

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2 // Не больше 2 задач одновременно

let operation1 = BlockOperation {
    // Загрузка первого изображения
    sleep(2)
    print("Загружено фото 1")
}

let operation2 = BlockOperation {
    // Загрузка второго изображения
    sleep(3)
    print("Загружено фото 2")
}

// operation2 начнётся только после завершения operation1
operation2.addDependency(operation1)

queue.addOperations([operation1, operation2], waitUntilFinished: false)

Это удобно, когда нужно выполнить задачи в строгом порядке или с ограничением ресурсов.

Современный async/await (Swift 5.5+)

Начиная с Swift 5.5, Apple представила встроенную поддержку асинхронности через async/await. Это делает код чище и читаемее — никаких замыканий и вложенных блоков.

Объявляем асинхронную функцию:

func fetchImage(from url: URL) async throws -> UIImage {
    let (data, _) = try await URLSession.shared.data(from: url)
    return UIImage(data: data)!
}

Используем её в коде:

Task {
    do {
        let image = try await fetchImage(from: url)
        // Возвращаемся на главный поток для UI
        await MainActor.run {
            self.imageView.image = image
        }
    } catch {
        print("Ошибка загрузки: \(error)")
    }
}

Обратите внимание на MainActor — это специальный актор, который гарантирует выполнение кода на главном потоке. Вам не нужно вручную вызывать DispatchQueue.main.async.

Сравнение подходов

Подход Плюсы Минусы
GCD Простота, обратная совместимость Вложенные блоки, легко ошибиться с потоком
OperationQueue Отмена задач, зависимости Более многословный код
async/await Чистый код, безопасность потоков Требует iOS 13+ / macOS 10.15+

Типичные ошибки при работе с background thread

Даже опытные разработчики иногда ошибаются. Вот топ-3 проблемы:

1. Обновление UI не на главном потоке

// НЕПРАВИЛЬНО
DispatchQueue.global().async {
    self.label.text = "Привет" // КРАШ!
}

// ПРАВИЛЬНО
DispatchQueue.global().async {
    DispatchQueue.main.async {
        self.label.text = "Привет"
    }
}

2. Блокировка главного потока синхронной операцией

// НЕПРАВИЛЬНО: sync блокирует текущий поток
DispatchQueue.main.sync {
    // Deadlock!
}

// ПРАВИЛЬНО: async
DispatchQueue.main.async {
    // Выполнится позже
}

3. Игнорирование thread sanitizer

В Xcode есть инструмент Thread Sanitizer (в схемах запуска). Включите его — он покажет все гонки данных и проблемы с потоками.

Практический пример: загрузка и кеширование изображений

Давайте соберём всё вместе. Напишем простой кеширующий загрузчик

Блоги

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