Swift Concurrency - это не просто синтаксический сахар. Под капотом происходит сложная трансформация кода, управляемая компилятором и рантаймом. Давайте разберём ключевые компоненты системы.
Компилятор и State Machine:
Когда компилятор встречает
async функцию, он разбивает ее на состояния (state machine). Каждый
await становится точкой приостановки, между которыми код делится на блоки.
func fetchData() async -> Data {
let data = await downloadData()
let processedData = await process(data)
return processedData
}
Suspension Points (Точки приостановки):
Каждый
await это место, где:
- Стек сохраняется в кучу (heap).
- Поток освобождается для других задач.
- Создаётся continuation (информация для возобновления).
Особенности:
- Приостановка не блокирует поток (в отличие от DispatchQueue).
- Swift Runtime управляет жизненным циклом данных.
Continuation (Продолжение):
Это объект, который хранит:
- Точку возобновления (какой await был последним).
- Локальные переменные.
- Что делать после получения результата.
Пример:
withCheckedContinuation { continuation in
someAsyncOperation { result in
continuation.resume(returning: result)
}
}
Executor (Исполнитель):
Определяет где и когда выполнять код после
await. Могут быть следующие варианты:
- Главным потоком (для MainActor).
- Глобальным исполнителем (для обычных async функций).
- Кастомным (для акторов).
Особенности:
- Если функция запущена на MainActor, продолжение тоже выполнится на нём.
- Глобальные функции используют кооперативный пул потоков.
Co-operative Thread Pool (Пул потоков):
Особенности:
- Фиксированное количество потоков (обычно = числу ядер CPU).
- Потоки не блокируются: при await они переключаются на другие задачи.
- Управляется Swift Runtime, а не операционной системой.
Почему это эффективно:
- Нет накладных расходов на создание потоков.
- Автоматическая балансировка нагрузки.
Как это работает вместе?
- Мы вызываем async функцию, после чего компилятор создаёт state machine.
- При первом await: состояние и переменные сохраняются в continuation, поток возвращается в пул.
- Когда результат готов, executor решает, где возобновить выполнение.
- State machine продолжает работу с последней точки.
Пример с потоками:
func loadData() async {
// 1. Выполняется на случайном потоке из пула
let data = await fetchFromNetwork() // 2. Поток освобождается
// 3. Возобновляется на том же или другом потоке
await MainActor.run {
self.data = data // 4. Гарантированно на главном потоке
}
}
Вывод:
- Компилятор разбивает код на состояния.
- Continuation хранит прогресс выполнения.
- Executor выбирает поток для возобновления.
- Пул потоков обеспечивает эффективное использование CPU.
Это делает
Swift Concurrency легковесным и масштабируемым, в отличие от традиционных потоков.