Введение: почему ваш 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, и переписывать его целиком нецелесообразно.
- Вам нуж