Очереди и фоновые задачи на Swift: пишем код, который не тормозит

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

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

Начать курс

Очереди и фоновые задачи на Swift: как заставить приложение летать

Представьте: вы открываете приложение для заказа пиццы, нажимаете «Посмотреть меню», а экран зависает на пять секунд. Вы начинаете нервничать, пицца кажется невкусной, приложение летит в корзину. Знакомая боль? Если вы разработчик на Swift, то наверняка сталкивались с ситуацией, когда интерфейс «тормозит» из-за тяжёлых операций — загрузки картинок, парсинга JSON или сложных вычислений. Секрет плавного UX кроется в правильной работе с очередями и фоновыми задачами.

В этой статье мы разберём, как организовать многопоточность в Swift: от классического GCD и OperationQueue до современного async/await. Вы узнаете, как выгрузить тяжёлую работу из главного потока, избежать дедлоков и написать код, который будет работать быстро и предсказуемо. Поехали!

Почему очередь — это не про скуку в магазине

В Swift очередь — это не то место, где вы стоите с корзиной. Очереди (DispatchQueue) — это механизм для управления задачами. Представьте, что у вас есть несколько поваров (потоков). Один повар обслуживает зал (главный поток — UI), а остальные готовят на кухне (фоновые потоки). Если заставить официанта жарить стейк, зал встанет. То же самое с интерфейсом: все обновления UI должны происходить только на главном потоке, а всё остальное — в фоне.

В Swift есть два типа очередей:

  • Serial (последовательная) — задачи выполняются одна за другой. Идеально для защиты общих ресурсов.
  • Concurrent (параллельная) — задачи стартуют одновременно. Отлично для загрузки изображений или параллельных вычислений.

Система сама управляет пулом потоков, поэтому вам не нужно создавать их вручную. Просто говорите: «Сделай это в фоне, а потом верни результат на главный». И Swift делает это элегантно.

GCD (Grand Central Dispatch) — старый друг лучше новых двух

Grand Central Dispatch — это низкоуровневый API, который появился ещё в Objective-C. Он жив и здоров, и его стоит знать каждому Swift-разработчику. Основная работа происходит через DispatchQueue.

Вот самый простой пример — загрузка данных в фоне и обновление UI:

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        loadImage()
    }
    
    func loadImage() {
        // Уходим в фоновую очередь
        DispatchQueue.global(qos: .background).async {
            // Тяжёлая работа: загрузка изображения
            guard let url = URL(string: "https://example.com/pizza.jpg"),
                  let data = try? Data(contentsOf: url),
                  let image = UIImage(data: data) else {
                return
            }
            
            // Возвращаемся на главный поток для обновления UI
            DispatchQueue.main.async {
                self.imageView.image = image
            }
        }
    }
}

Обратите внимание на qos (Quality of Service). Это приоритет задачи: .userInteractive — для анимаций, .background — для синхронизации, .utility — для индикаторов прогресса. Правильный выбор QoS помогает системе распределять ресурсы.

DispatchGroup: ждём всех, а не одного

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

func loadMultipleImages() {
    let group = DispatchGroup()
    var images: [UIImage] = []
    
    let urls = ["url1", "url2", "url3"]
    
    for urlString in urls {
        group.enter() // Входим в группу
        DispatchQueue.global().async {
            // Загрузка изображения
            if let url = URL(string: urlString),
               let data = try? Data(contentsOf: url),
               let image = UIImage(data: data) {
                images.append(image)
            }
            group.leave() // Выходим из группы
        }
    }
    
    // Уведомление о завершении всех задач
    group.notify(queue: .main) {
        self.imageView.image = images.first
        print("Все изображения загружены!")
    }
}

Используйте enter() и leave() парно — иначе группа никогда не завершится, и вы получите утечку памяти.

OperationQueue: когда нужен контроль над задачами

OperationQueue — это более высокоуровневая абстракция над GCD. Она даёт вам возможность отменять задачи, задавать зависимости и ограничивать количество параллельных операций. Если вы пишете сложный пайплайн обработки данных, OperationQueue — ваш выбор.

Пример: загружаем изображение, фильтруем его и сохраняем. Каждый шаг — отдельная операция, и они зависят друг от друга.

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

let loadOperation = BlockOperation {
    // Загрузка данных
    Thread.sleep(forTimeInterval: 1) // Имитация задержки
    print("Загрузка завершена")
}

let filterOperation = BlockOperation {
    // Применение фильтра
    print("Фильтрация завершена")
}

let saveOperation = BlockOperation {
    // Сохранение результата
    print("Сохранение завершено")
}

// Устанавливаем зависимости
filterOperation.addDependency(loadOperation)
saveOperation.addDependency(filterOperation)

queue.addOperations([loadOperation, filterOperation, saveOperation], waitUntilFinished: false)

Прелесть OperationQueue в том, что вы можете отменить все операции, если пользователь ушёл с экрана:

queue.cancelAllOperations()

Или проверить, не отменена ли операция внутри неё:

let operation = BlockOperation {
    if operation.isCancelled { return }
    // Тяжёлая работа
}

Современный Swift: async/await — пишем как на Python, но быстрее

Начиная с Swift 5.5, в язык пришла модель structured concurrency с ключевыми словами async и await. Если вы знакомы с Python, то почувствуете себя как дома. Код становится линейным, без вложенных замыканий, а ошибки обрабатываются через обычные do-catch.

Перепишем наш первый пример с загрузкой изображения на modern Swift:

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            await loadImageAsync()
        }
    }
    
    func loadImageAsync() async {
        guard let url = URL(string: "https://example.com/pizza.jpg") else { return }
        
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            if let image = UIImage(data: data) {
                // UI обновляем на главном акторе
                await MainActor.run {
                    self.imageView.image = image
                }
            }
        } catch {
            print("Ошибка загрузки: \(error)")
        }
    }
}

Обратите внимание: мы используем Task для запуска

Блоги

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