Почему фоновые потоки — это боль каждого 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 (в схемах запуска). Включите его — он покажет все гонки данных и проблемы с потоками.
Практический пример: загрузка и кеширование изображений
Давайте соберём всё вместе. Напишем простой кеширующий загрузчик