С 2024-2025 годов Swift Concurrency спрашивают на каждом iOS-собесе уровня middle и выше: async/await, Task, акторы, Sendable и что изменилось в Swift 6.
async/await
func loadProfile() async throws -> Profile {
let (data, response) = try await URLSession.shared.data(from: url)
try validate(response)
return try JSONDecoder().decode(Profile.self, from: data)
}
await отмечает точку возможной приостановки. Пока результата нет, поток не блокируется, а выполняет другую работу. Продолжение может выполниться на другом потоке, если функция не изолирована на актор (например, на MainActor).
Вызвать async-функцию из синхронного кода можно через Task:
@MainActor func buttonTapped() {
Task { await viewModel.refresh() }
}
Структурированная конкурентность
async let для фиксированного числа параллельных операций:
async let profile = api.profile()
async let feed = api.feed()
let (p, f) = try await (profile, feed)
withTaskGroup для заранее неизвестного числа:
let images = await withTaskGroup(of: (URL, UIImage?).self) { group in
for url in urls { group.addTask { (url, await load(url)) } }
var result: [URL: UIImage] = [:]
for await (url, image) in group { result[url] = image }
return result
}
Родитель ждёт всех детей, ошибка или отмена родителя отменяет детей. Это и есть «структурированность»: задачи образуют дерево и не теряются.
Task и Task.detached
Task { }наследует контекст места создания: изоляцию актора, приоритет, task-local значения. Из@MainActor-кода тело выполняется на главном акторе.Task.detached { }ничего не наследует. Нужен редко.
Обе неструктурированные: никто не ждёт и не отменяет их автоматически. Ссылку сохраняют, если задачу нужно отменить.
Отмена
Отмена кооперативная: task.cancel() ставит флаг. Код проверяет его сам (Task.isCancelled, try Task.checkCancellation()), а многие системные API (Task.sleep, URLSession) бросают ошибку при отмене. В SwiftUI модификатор .task отменяет задачу при исчезновении view.
Акторы
Актор это ссылочный тип, чьё изменяемое состояние доступно только изнутри. Методы выполняются по одному, поэтому гонок данных нет. Снаружи обращения асинхронные:
actor TokenStore {
private var token: String?
func set(_ t: String) { token = t }
func get() -> String? { token }
}
let t = await store.get()
Реентерабельность. На каждом await внутри метода актор отпускается и может выполнить другие вызовы. Значит, состояние, проверенное до await, после него может измениться:
actor ImageCache {
private var tasks: [URL: Task<UIImage, Error>] = [:]
func image(_ url: URL) async throws -> UIImage {
if let task = tasks[url] { return try await task.value }
let task = Task { try await download(url) }
tasks[url] = task // кладём до await
return try await task.value
}
}
Хранение Task вместо результата склеивает одновременные запросы одного URL.
MainActor
@MainActor это глобальный актор главного потока. Помечают view model и всё, что трогает UI. С SDK iOS 18 протокол View в SwiftUI изолирован на @MainActor. В Swift 6.2 появилась настройка модуля Default Actor Isolation = MainActor: весь код без аннотаций считается изолированным на главном акторе, а фоновую работу помечают явно (nonisolated, @concurrent).
Sendable
Sendable помечает типы, которые безопасно передавать между доменами изоляции (задачами, акторами):
- структуры и enum, у которых все поля Sendable (внутри модуля соответствие выводится само);
- акторы;
final classс толькоlet-свойствами Sendable-типов;@unchecked Sendableдля типов, которые вы синхронизируете сами (например, блокировкой).
@Sendable-замыкание захватывает только Sendable-значения и не мутирует захваченные переменные.
Swift 6: безопасность от гонок
В режиме Swift 6 проверки изоляции и Sendable становятся ошибками компиляции. Компилятор Swift 6 умеет и режим Swift 5, поэтому переходят помодульно:
- Включить строгие проверки (complete) в режиме Swift 5, собрать предупреждения.
- Изолировать UI на
@MainActor, общее состояние спрятать в акторы. - Сделать модели Sendable.
- Для старых зависимостей
@preconcurrency import. - Переключить модуль на Swift 6.
Мосты из старого кода
func legacyLoad() async throws -> Data {
try await withCheckedThrowingContinuation { cont in
oldLoad { result in cont.resume(with: result) }
}
}
Продолжение возобновляют ровно один раз. Для потоков событий от делегатов есть AsyncStream с yield и finish.
Чего нельзя делать
- Блокировать поток пула:
DispatchSemaphore.wait(),Thread.sleep, синхронные сетевые вызовы. Пул размером с число ядер, и блокировка может подвесить всё. - Использовать
Task.detachedпо привычке. - Считать, что актор защищает от логических гонок через
await.
Что спросят на собесе
- Что происходит на
await, на каком потоке продолжится функция? - Разница
TaskиTask.detached,async letиTaskGroup. - Что такое актор, реентерабельность и как с ней жить?
- Что такое Sendable, что меняет режим Swift 6?
- Как обернуть callback-API в async?