Async/Await в Swift: полное руководство по структурированной конкурентности
Современная разработка под Apple-экосистему требует эффективной работы с асинхронными операциями: загрузка данных из сети, работа с файлами, обработка изображений. До появления async/await разработчики использовали замыкания (completion handlers) или Combine, что приводило к "callback hell" и сложному управлению состоянием. Swift 5.5 представил встроенную поддержку асинхронных функций, изменив подход к конкурентному программированию.
Async/Await — это не библиотека, а встроенная языковая конструкция Swift, позволяющая писать асинхронный код так же просто, как синхронный. Ключевые слова async и await делают конкурентный код линейным, читаемым и безопасным. Вместо вложенных замыканий вы пишете последовательные вызовы, а компилятор сам управляет переключением контекстов.
Эта статья — полное руководство по async/await в Swift. Вы узнаете, как установить минимальную версию Swift, освоите основные методы (Task, TaskGroup, Actor, async let и другие), увидите реальный пример приложения для загрузки и обработки изображений, а также получите советы по производительности и типичным ошибкам.
1. Что это и зачем нужно?
Async/Await — это модель конкурентности, встроенная в язык Swift начиная с версии 5.5. Она позволяет объявлять функции как async (асинхронные) и приостанавливать их выполнение до получения результата с помощью await. Это решает три ключевые проблемы:
- Читаемость: код выглядит как синхронный, без вложенных замыканий.
- Безопасность: компилятор проверяет, что асинхронные функции вызываются только в асинхронном контексте.
- Производительность: эффективное использование потоков без ручного управления.
Зачем это нужно? Представьте, что вам нужно загрузить три изображения, обработать их и сохранить. Без async/await вы бы писали цепочку замыканий с обработкой ошибок. С async/await это три строки с await и одним do-catch.
2. Установка
Async/Await — это часть языка Swift, поэтому не требует установки дополнительных библиотек. Вам нужен только Xcode 13+ или Swift 5.5+.
Проверка версии Swift
swift --version
# Apple Swift version 5.9 (swiftlang-5.9.0.128.2 clang-1500.0.40.1)
# Target: arm64-apple-macosx14.0
Минимальные требования
- Xcode: 13.0+ (рекомендуется 15.0+)
- Swift: 5.5+ (рекомендуется 5.9+)
- Платформы: iOS 13+, macOS 10.15+, watchOS 6+, tvOS 13+
AsyncSequence) требуют iOS 15+ / macOS 12+.
Создание нового проекта
# Swift Package Manager
mkdir AsyncDemo
cd AsyncDemo
swift package init --type executable
# Откройте Package.swift и укажите swift-tools-version: 5.7+
Или просто создайте новый playground в Xcode и выберите шаблон "iOS" или "macOS".
3. Быстрый старт — минимальный рабочий пример
Напишем простую программу, которая загружает данные из сети и выводит их длину.
import Foundation
// Асинхронная функция для загрузки данных
func fetchData(from urlString: String) async throws -> Data {
guard let url = URL(string: urlString) else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Основная асинхронная точка входа
@main
struct AsyncDemoApp {
static func main() async {
do {
let data = try await fetchData(from: "https://jsonplaceholder.typicode.com/posts/1")
print("Загружено \(data.count) байт")
if let text = String(data: data, encoding: .utf8) {
print("Содержимое: \(text.prefix(100))...")
}
} catch {
print("Ошибка: \(error.localizedDescription)")
}
}
}
Как это работает:
- Функция
fetchDataпомечена какasync throws— она может приостанавливаться и выбрасывать ошибки. await URLSession.shared.data(from: url)приостанавливает выполнение до завершения сетевого запроса.- Точка входа
main() asyncпозволяет запускать асинхронный код из командной строки.
4. Основные методы, функции и классы
Рассмотрим 10 ключевых элементов async/await в Swift. Каждый включает сигнатуру, описание и пример.
4.1. async — объявление асинхронной функции
Сигнатура: func имя(параметры) async -> Тип
Что делает: Помечает функцию как асинхронную. Такая функция может приостанавливать своё выполнение с помощью await.
func calculatePiAsync(iterations: Int) async -> Double {
var pi: Double = 0
for k in 0..<iterations {
pi += Double((k % 2 == 0 ? 1 : -1)) / Double(2 * k + 1)
// Искусственная приостановка для демонстрации
if k % 100000 == 0 {
await Task.yield()
}
}
return pi * 4
}
// Использование
let pi = await calculatePiAsync(iterations: 1000000)
print("Pi ≈ \(pi)")
4.2. await — ожидание асинхронного результата
Сигнатура: let результат = await асинхронныйВызов()
Что делает: Приостанавливает текущую задачу до завершения асинхронной операции, не блокируя поток.
func downloadAndProcess() async {
let data = await fetchData(from: "https://example.com/data.json")
let processed = await processData(data)
print("Результат: \(processed)")
}
// Примечание: await можно использовать только внутри async-контекста
4.3. Task — создание новой асинхронной задачи
Сигнатура: Task(priority: TaskPriority?, operation: () async -> Success)
Что делает: Создаёт новую асинхронную задачу, выполняющуюся в параллельном контексте. Возвращает Task, который можно отменить или дождаться.
func runTasks() async {
// Задача с высоким приоритетом
let task1 = Task(priority: .high) {
return await fetchData(from: "https://api.example.com/urgent")
}
// Задача с фоновым приоритетом
let task2 = Task(priority: .background) {
return await fetchData(from: "https://api.example.com/background")
}
// Ожидание результатов
let result1 = await task1.value
let result2 = await task2.value
print("Оба результата получены")
}
4.4. TaskGroup — группа параллельных задач
Сигнатура: withTaskGroup(of: ChildTaskResult, returning: GroupResult, body: (inout TaskGroup) async -> GroupResult)
Что делает: Позволяет запускать несколько параллельных задач и собирать их результаты. Все задачи в группе выполняются конкурентно.
func downloadAllImages(urls: [URL]) async throws -> [Data] {
return try await withTaskGroup(of: Data.self) { group in
var results: [Data] = []
for url in urls {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
// Сбор результатов по мере завершения
for try await data in group {
results.append(data)
}
return results
}
}
4.5. async let — конкурентное связывание
Сигнатура: async let имя = выражение
Что делает: Запускает асинхронную операцию параллельно с текущим контекстом. Результат становится доступен при первом использовании await.
func fetchUserAndPosts() async throws -> (User, [Post]) {
async let user = fetchUser()
async let posts = fetchPosts()
// Обе задачи выполняются параллельно
return try await (user, posts)
}
// Использование
let (user, posts) = try await fetchUserAndPosts()
print("Пользователь: \(user.name), постов: \(posts.count)")
4.6. Actor — потокобезопасный тип
Сигнатура: actor Имя { свойства и методы }
Что делает: Защищает состояние от гонок данных. Все методы actor выполняются последовательно на одном исполнителе.
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
// Использование
let counter = Counter()
await counter.increment()
let current = await counter.getValue()
print("Счётчик: \(current)")
4.7. MainActor — выполнение на главном потоке
Сигнатура: @MainActor func имя() или await MainActor.run { ... }
Что делает: Гарантирует выполнение кода на главном потоке (UI-потоке). Критически важно для обновления интерфейса.
@MainActor
class ViewModel: ObservableObject {
@Published var text = ""
func updateUI() async {
// Этот метод гарантированно выполняется на главном потоке
let data = await fetchData(from: "https://api.example.com")
text = String(data: data, encoding: .utf8) ?? ""
}
}
// Альтернативный способ
await MainActor.run {
self.label.text = "Обновлено"
}
4.8. AsyncSequence — асинхронная последовательность
Сигнатура: protocol AsyncSequence { associatedtype Element; func makeAsyncIterator() -> AsyncIterator }
Что делает: Позволяет итерировать элементы, которые становятся доступны асинхронно (например, строки из файла или события из сети).
import Foundation
// Чтение файла построчно с AsyncSequence
func readLines(from url: URL) -> AsyncLine