Конкурентность в Swift: потоки, горутины и async/await — как не сойти с ума

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

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

Начать курс

Введение: почему ваш iPhone не зависает, пока вы скроллите ленту?

Вы когда-нибудь задумывались, почему современные iOS-приложения не тормозят, даже когда одновременно загружают десятки картинок, обрабатывают нажатия и пишут данные в базу? Ответ прост — конкурентность. В мире Swift (и не только) это одна из самых горячих тем. И если вы пишете на Swift, но до сих пор путаетесь в потоках, горутинах и новомодном async/await — добро пожаловать в клуб. Сегодня мы разложим всё по полочкам, чтобы ваш код летал, а не ползал.

В этой статье мы не просто перечислим сухие определения. Мы посмотрим на реальные примеры, сравним подходы и ответим на главный вопрос: что выбрать для своего проекта — старый добрый GCD или новый Swift Concurrency?

Потоки: фундамент, на котором всё держится

Начнём с базы. Поток (Thread) — это наименьшая единица обработки, которую может выполнять операционная система. Представьте себе конвейер: один поток — одна линия, по которой движутся задачи. Если у вас одно ядро процессора, то потоки будут выполняться по очереди, но очень быстро переключаясь, создавая иллюзию параллельности. На многоядерных процессорах потоки могут выполняться реально параллельно.

В Swift долгое время основным инструментом для работы с потоками был Grand Central Dispatch (GCD). Вы просто создавали очередь и отправляли в неё замыкание:

DispatchQueue.global(qos: .background).async {
    // Тяжелая работа в фоне
    let result = heavyComputation()
    DispatchQueue.main.async {
        // Обновление UI на главном потоке
        self.label.text = result
    }
}

Всё просто, правда? Но у GCD есть нюансы:

  • Вы сами отвечаете за возвращение на главный поток (легко забыть и получить креш).
  • Нет встроенной поддержки отмены задач (приходится писать флаги вручную).
  • Сложно обрабатывать цепочки зависимых операций без вложенности замыканий (так называемый «callback hell»).

Тем не менее, GCD до сих пор жив и используется в legacy-проектах. Но современный Swift предлагает нечто гораздо более элегантное.

Горутины в Swift? Нет, это не Go

Когда говорят о горутинах, обычно вспоминают Go. В Swift прямого аналога горутин нет, но есть легковесные задачи (tasks), которые ведут себя очень похоже. Они не привязаны к конкретному потоку — система сама решает, на каком потоке выполнять задачу, и может переключать их между потоками без создания новых на каждый вызов.

В отличие от потоков, которые «весят» около 8 КБ стека каждая, задачи Swift Concurrency практически ничего не весят. Вы можете запустить тысячи задач без риска переполнить память. Это и есть та самая «конкурентность без боли».

Посмотрите, как это выглядит:

Task {
    let image = await downloadImage(from: url)
    await MainActor.run {
        self.imageView.image = image
    }
}

Заметили? Никаких DispatchQueue.main.async. Просто await MainActor.run. Чисто, понятно, безопасно.

async/await: революция, которая случилась

С выходом Swift 5.5 появилась встроенная поддержка async/await. Это не просто синтаксический сахар — это смена парадигмы. Теперь вы можете писать асинхронный код так, как будто он синхронный, но при этом не блокировать поток.

Объявить асинхронную функцию просто:

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: \(user.name)")
    } catch {
        print("Failed to fetch user: \(error)")
    }
}

Ключевое слово await говорит компилятору: «Приостанови выполнение этой функции, но не блокируй поток. Когда результат будет готов, продолжи». Это позволяет системе эффективно использовать ресурсы, переключаясь на другие задачи во время ожидания.

Почему это лучше GCD?

  • Читаемость: код выглядит как последовательный, без вложенных замыканий.
  • Безопасность: компилятор контролирует, что вы не забыли переключиться на главный поток.
  • Отмена: задачи можно отменять целиком, включая все дочерние операции.

Structured Concurrency: порядок в хаосе

Одна из главных фишек Swift Concurrency — structured concurrency. Идея в том, что каждая асинхронная задача имеет чёткое время жизни, ограниченное областью видимости. Вы не можете просто так «запустить задачу и забыть» — компилятор требует, чтобы вы дождались её завершения или явно указали, что задача не должна быть отменена.

Пример с использованием async let для параллельного выполнения:

func loadFullProfile() async throws -> Profile {
    // Эти две задачи запускаются параллельно
    async let user = fetchUser()
    async let posts = fetchPosts()
    // Ждём обе
    return try await Profile(user: user, posts: posts)
}

Это мощный инструмент. Теперь вы точно знаете, что обе задачи будут выполнены (или отменены), и не получите утечку ресурсов.

Actors: защита от гонки данных

Гонка данных (data race) — классическая проблема многопоточности. Когда два потока одновременно пишут в одну переменную, результат непредсказуем. В Swift Concurrency эту проблему решают акторы (actors).

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

actor Counter {
    private var value = 0
    
    func increment() {
        value += 1
    }
    
    func getValue() -> Int {
        return value
    }
}

let counter = Counter()
Task {
    await counter.increment()
    let current = await counter.getValue()
    print(current)
}

Обратите внимание: все вызовы методов актора — асинхронные (await). Это не случайность — так компилятор гарантирует, что доступ к данным будет потокобезопасным.

Когда что использовать? Практические советы

Теперь, когда мы разобрали все инструменты, давайте честно ответим на вопрос: что выбрать для своего проекта?

GCD стоит использовать, если:

  • Вы поддерживаете старые версии iOS (до 13.0).
  • У вас уже есть большой legacy-код на GCD, и переписывать его целиком нецелесообразно.
  • Вам нуж

Блоги

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