Конкурентность и многопоточность в Swift: полное руководство

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

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

Начать курс

Введение в конкурентность и многопоточность в Swift

Современные приложения для iOS и macOS должны быть быстрыми и отзывчивыми. Пользователь не должен ждать, пока загрузится изображение или выполнится сложный расчет. Для этого в Swift существуют механизмы конкурентности и многопоточности. В этой статье мы разберем ключевые концепции, начиная от классического Grand Central Dispatch (GCD) и заканчивая современным async/await из Swift Concurrency.

Конкурентность (concurrency) — это способность программы выполнять несколько задач одновременно, переключаясь между ними. Многопоточность (multithreading) — это одна из реализаций конкурентности, где задачи выполняются в разных потоках операционной системы. Swift предоставляет мощные и безопасные инструменты для работы с этими концепциями.

1. Grand Central Dispatch (GCD) — основа многопоточности

Grand Central Dispatch (GCD) — это низкоуровневая библиотека Apple для управления параллельным выполнением кода. Она работает на основе очередей (dispatch queues).

Типы очередей в GCD

  • Serial Queue (последовательная очередь) — задачи выполняются одна за другой в порядке добавления.
  • Concurrent Queue (параллельная очередь) — задачи могут выполняться одновременно на разных потоках.
  • Main Queue (главная очередь) — выполняет задачи на главном потоке, предназначена для обновления UI.
  • Global Queues (глобальные очереди) — системные параллельные очереди с разными приоритетами (QoS).

Пример: выполнение задачи в фоне и обновление UI

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!

    func loadImage(from url: URL) {
        // Переключаемся на фоновую очередь
        DispatchQueue.global(qos: .background).async { [weak self] in
            guard let data = try? Data(contentsOf: url),
                  let image = UIImage(data: data) else {
                return
            }
            
            // Возвращаемся на главный поток для обновления UI
            DispatchQueue.main.async {
                self?.imageView.image = image
            }
        }
    }
}

В этом примере мы скачиваем изображение в фоновом потоке, чтобы не блокировать интерфейс, а затем обновляем UI на главном потоке.

DispatchGroup для синхронизации задач

DispatchGroup позволяет отслеживать выполнение группы асинхронных задач и выполнить действие после их завершения.

let group = DispatchGroup()

group.enter()
fetchData1 { result in
    print("Данные 1 получены")
    group.leave()
}

group.enter()
fetchData2 { result in
    print("Данные 2 получены")
    group.leave()
}

group.notify(queue: .main) {
    print("Все данные загружены, обновляем UI")
}

2. OperationQueue — объектно-ориентированный подход

OperationQueue — это более высокоуровневая абстракция поверх GCD. Она работает с объектами Operation, которые представляют собой единицу работы.

Преимущества OperationQueue

  • Возможность отмены задач (cancel).
  • Установка зависимостей между операциями (addDependency).
  • Мониторинг состояния операции (isFinished, isExecuting, isCancelled).
  • Настройка максимального количества параллельных операций.

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

import UIKit

class ImageLoader {
    private let queue = OperationQueue()
    
    func loadImages(from urls: [URL], completion: @escaping ([UIImage]) -> Void) {
        var images: [UIImage] = []
        let completionOperation = BlockOperation {
            DispatchQueue.main.async {
                completion(images)
            }
        }
        
        for url in urls {
            let operation = BlockOperation {
                if let data = try? Data(contentsOf: url),
                   let image = UIImage(data: data) {
                    images.append(image)
                }
            }
            completionOperation.addDependency(operation)
            queue.addOperation(operation)
        }
        
        queue.addOperation(completionOperation)
    }
}

В этом примере мы создаем несколько операций загрузки, а затем одну операцию завершения, которая выполняется только после загрузки всех изображений.

3. Swift Concurrency: async/await и Task

Начиная с Swift 5.5, Apple представила встроенную поддержку асинхронного программирования через ключевые слова async и await. Это делает код более читаемым и менее подверженным ошибкам, характерным для GCD (например, retain cycles).

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

func fetchUserData() async throws -> User {
    let url = URL(string: "https://api.example.com/user")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

Вызов асинхронной функции

Task {
    do {
        let user = try await fetchUserData()
        print("Пользователь: \(user.name)")
    } catch {
        print("Ошибка: \(error)")
    }
}

Контекст Task создает новую асинхронную задачу. Внутри него можно использовать await для ожидания результатов других асинхронных функций.

Параллельное выполнение с async let

Если нужно запустить несколько независимых задач одновременно, используйте async let:

func loadDashboardData() async throws -> Dashboard {
    async let user = fetchUserData()
    async let posts = fetchUserPosts()
    async let settings = fetchUserSettings()
    
    return try await Dashboard(
        user: user,
        posts: posts,
        settings: settings
    )
}

Все три функции запускаются параллельно, и результат возвращается только после завершения всех трех.

Акторы (Actors) для защиты данных

Акторы — это новый механизм Swift для защиты от гонки данных (data race). Они гарантируют, что доступ к их свойствам происходит последовательно, даже из разных задач.

actor DataStore {
    private var cache: [String: Data] = [:]
    
    func store(key: String, value: Data) {
        cache[key] = value
    }
    
    func retrieve(key: String) -> Data? {
        return cache[key]
    }
}

// Использование
let store = DataStore()
await store.store(key: "image1", value: imageData)
let data = await store.retrieve(key: "image1")

4. Сравнение подходов и рекомендации

Подход Когда использовать
GCD (DispatchQueue) Простые задачи, одноразовые операции, быстрый переключатель между потоками.
OperationQueue Сложные зависимости

Блоги

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