До Swift 5.5 асинхронный код на iOS писали через замыкания, DispatchQueue и Combine. Это работало, но читать такой код — отдельное удовольствие: вложенные колбэки, ручное переключение между потоками, утечки памяти из-за забытых [weak self]. Swift Concurrency появился как ответ на этот хаос — и за три года стал стандартом.
Эта статья — практический разбор. Не «что такое async/await теоретически», а как это выглядит в реальном коде, где работает хорошо, а где нужно быть осторожным.
Зачем вообще нужен async/await
Рассмотрим типичную задачу: загрузить данные пользователя, потом загрузить его посты, потом показать всё на экране.
Старый способ через замыкания:
fetchUser(id: userId) { result in
switch result {
case .success(let user):
fetchPosts(for: user.id) { result in
switch result {
case .success(let posts):
DispatchQueue.main.async {
self?.updateUI(user: user, posts: posts)
}
case .failure(let error):
DispatchQueue.main.async {
self?.showError(error)
}
}
}
case .failure(let error):
DispatchQueue.main.async {
self?.showError(error)
}
}
}
То же самое с async/await:
func loadUserData() async throws {
let user = try await fetchUser(id: userId)
let posts = try await fetchPosts(for: user.id)
await MainActor.run {
updateUI(user: user, posts: posts)
}
}
Разница очевидна. Код читается сверху вниз, обработка ошибок через throws в одном месте, никаких вложенных колбэков.
Как это работает под капотом
Async/await — это не магия и не новые потоки. Swift использует механизм continuation: когда функция встречает await, она приостанавливается и освобождает поток. Когда ответ приходит, выполнение продолжается — возможно, на другом потоке из пула.
Важный момент: await не блокирует поток. Это ключевое отличие от Thread.sleep или семафоров. Поток свободен и может выполнять другую работу пока идёт ожидание.
Swift Concurrency использует кооперативный пул потоков. Вместо создания нового потока на каждую задачу (как делал GCD в худших случаях), система управляет фиксированным количеством потоков — обычно равным числу ядер процессора. Это снижает накладные расходы на переключение контекста.
Task — основа всего
Чтобы вызвать async-функцию из синхронного контекста, нужен Task:
class ProfileViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
try await loadUserData()
} catch {
showError(error)
}
}
}
}
Task запускает асинхронную работу и наследует приоритет от контекста. Если создаёте Task внутри MainActor-контекста (а UIViewController — это MainActor), задача тоже будет выполняться на главном потоке, если не указать иное.
Task с приоритетом
Task(priority: .background) {
await processHeavyData()
}
Доступные приоритеты: .high, .medium, .low, .background, .utility, .userInitiated. Система учитывает их при планировании, но не гарантирует строгий порядок.
Отмена задачи
var loadTask: Task<Void, Never>?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
loadTask?.cancel()
}
override func viewDidLoad() {
super.viewDidLoad()
loadTask = Task {
await loadData()
}
}
Отмена в Swift Concurrency — кооперативная. Task не убивается принудительно, он получает флаг isCancelled. Ваш код должен этот флаг проверять:
func processItems(_ items: [Item]) async throws {
for item in items {
try Task.checkCancellation() // бросает CancellationError если задача отменена
await process(item)
}
}
Параллельное выполнение с async let
Если нужно загрузить несколько независимых вещей одновременно, async let делает это элегантно:
func loadDashboard() async throws {
async let user = fetchUser(id: userId)
async let stats = fetchStats(for: userId)
async let notifications = fetchNotifications()
// Всё три запроса идут параллельно
let (userData, statsData, notificationsData) = try await (user, stats, notifications)
updateDashboard(user: userData, stats: statsData, notifications: notificationsData)
}
Без async let те же три запроса шли бы последовательно. Если каждый занимает 300мс, разница между 900мс и ~300мс ощутима.
TaskGroup — когда задач много
async let удобен для фиксированного числа задач. Для динамических коллекций — TaskGroup:
func downloadImages(urls: [URL]) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: UIImage.self) { group in
for url in urls {
group.addTask {
try await downloadImage(from: url)
}
}
var images: [UIImage] = []
for try await image in group {
images.append(image)
}
return images
}
}
Все задачи в группе запускаются параллельно. Если одна бросает ошибку, группа отменяет остальные — поведение можно настроить.
Actor — защита от гонок данных
Одна из главных проблем многопоточности — race condition: два потока одновременно читают и пишут одни данные. Swift решает это через actor.
actor UserCache {
private var cache: [String: User] = [:]
func get(id: String) -> User? {
cache[id]
}
func set(_ user: User, for id: String) {
cache[id] = user
}
}
// Использование
let cache = UserCache()
Task {
let user = await cache.get(id: "123")
}
Компилятор гарантирует: к свойствам актора можно обратиться только через await. Одновременно внутри актора выполняется не более одной задачи — взаимное исключение встроено в систему типов.
MainActor
MainActor — встроенный актор для главного потока. Обновлять UI нужно именно на нём:
@MainActor
class ProfileViewModel: ObservableObject {
@Published var user: User?
@Published var isLoading = false
func loadUser() async {
isLoading = true
defer { isLoading = false }
do {
user = try await userService.fetchUser()
} catch {
// обработка ошибки
}
}
}
Аннотация @MainActor на классе означает: все методы и свойства этого класса выполняются на главном потоке. Компилятор проверяет это на этапе сборки — никаких сюрпризов в runtime.
Интеграция со старым кодом
Не весь код можно переписать сразу. Часто нужно обернуть колбэк-based API в async-функцию. Для этого есть withCheckedContinuation:
func fetchData() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
oldAPIClient.fetch { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
Правило: continuation нужно вызвать ровно один раз. Если вызвать дважды — краш. Если не вызвать — утечка. withCheckedContinuation помогает: в Debug-сборках предупреждает если continuation не был вызван.
Типичные ошибки
Ошибка 1: Создавать слишком много Task
// Плохо — N задач на N элементов без ограничений
for item in hugeArray {
Task {
await process(item)
}
}
// Лучше — TaskGroup с контролем параллелизма
await withTaskGroup(of: Void.self) { group in
for item in hugeArray {
group.addTask { await process(item) }
}
}
TaskGroup автоматически управляет ресурсами лучше, чем сотни независимых Task.
Ошибка 2: Блокирующий код внутри async-функции
// Плохо — блокирует поток из кооперативного пула
func processData() async {
Thread.sleep(forTimeInterval: 2) // НЕЛЬЗЯ
let result = heavyComputation() // если занимает секунды — тоже проблема
}
// Лучше — вынести в detached task или использовать await Task.sleep
func processData() async {
await Task.sleep(nanoseconds: 2_000_000_000)
}
Кооперативный пул ожидает, что задачи отдают управление через await. Если код блокирует поток — вся система страдает.
Ошибка 3: Игнорировать отмену
Если пользователь ушёл с экрана, а Task продолжает работать — это трата ресурсов. Всегда проверяйте Task.isCancelled в длинных циклах и вызывайте Task.checkCancellation() в критических точках.
Structured vs Unstructured Concurrency
Swift Concurrency делит задачи на два вида:
Structured — async let, TaskGroup. Задачи живут в рамках родительского контекста: если родитель отменяется, дочерние тоже. Время жизни задачи ограничено областью видимости.
Unstructured — Task { }, Task.detached { }. Задача независима от контекста создания. Task.detached не наследует даже актор и приоритет — чистый лист.
Отдавайте предпочтение structured concurrency там, где можно. Она безопаснее: компилятор отслеживает время жизни задач и помогает избежать утечек.
Реальный пример: ViewModel для списка
@MainActor
final class ArticleListViewModel: ObservableObject {
@Published var articles: [Article] = []
@Published var state: LoadingState = .idle
private let repository: ArticleRepository
private var loadTask: Task<Void, Never>?
init(repository: ArticleRepository) {
self.repository = repository
}
func load() {
loadTask?.cancel()
loadTask = Task {
state = .loading
do {
articles = try await repository.fetchArticles()
state = .loaded
} catch is CancellationError {
// задача отменена — ничего не делаем
} catch {
state = .error(error)
}
}
}
func refresh() async {
do {
articles = try await repository.fetchArticles()
} catch {
state = .error(error)
}
}
deinit {
loadTask?.cancel()
}
}
Этот паттерн покрывает большинство случаев: отмена при повторной загрузке, обработка CancellationError отдельно от других ошибок, явное управление состоянием.
Где Swift Concurrency реально спасает
Несколько сценариев, где переход с GCD на async/await ощущается сильнее всего:
Цепочки зависимых запросов — вместо пирамиды колбэков получаете линейный код. Читаемость вырастает кратно.
Параллельные независимые запросы — async let делает это в две строки без DispatchGroup и семафоров.
Тестирование — async-функции тестируются напрямую через async throws в тестах, без expectation и wait.
Отмена — структурная отмена работает автоматически. В GCD-коде отмену нередко просто не реализовывали — слишком муторно.
Если вы разрабатываете iOS-приложение с нуля в 2026 году, Swift Concurrency — это не опция, это baseline. Команды в REEXY, которые берутся за мобильную разработку, пишут на async/await по умолчанию: меньше багов, проще код-ревью, быстрее онбординг.
Совместимость
Swift Concurrency доступен с iOS 13 при использовании back-deployment (Swift 5.5.2+, Xcode 13.2+). Для более старых версий iOS некоторые API недоступны — например, AsyncStream требует iOS 15. Всегда проверяйте минимальную версию deployment target перед активным использованием новых API.
Actor isolation проверяется компилятором — это значит ошибки гонок данных ловятся на этапе сборки, а не в production. Для проекта с командой из нескольких разработчиков это существенно снижает класс багов, которые вообще могут добраться до пользователей.