Введение в асинхронное программирование на Swift
Современные мобильные и десктопные приложения на Swift (iOS/macOS) всё чаще требуют выполнения длительных операций — загрузки данных из сети, работы с файлами, обработки изображений — без блокировки пользовательского интерфейса. До недавнего времени разработчики использовали замыкания (completion handlers) и Grand Central Dispatch (GCD), что приводило к так называемому «аду колбэков» и усложняло чтение кода. С выходом Swift 5.5 компания Apple представила принципиально новую модель асинхронного программирования, основанную на ключевых словах async и await, а также на акторах и структурах Task. В этом руководстве мы разберём все ключевые концепции на практических примерах.
1. Основы async/await в Swift
Ключевое слово async указывает, что функция или метод могут приостанавливать своё выполнение для ожидания результата асинхронной операции. Вызов такой функции помечается словом await. Это позволяет писать асинхронный код так же линейно, как синхронный.
// Объявляем асинхронную функцию
func fetchUserData(from url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Используем её в контексте задачи
Task {
do {
let url = URL(string: "https://api.example.com/user")!
let userData = try await fetchUserData(from: url)
print("Данные получены: \(userData.count) байт")
} catch {
print("Ошибка загрузки: \(error.localizedDescription)")
}
}
Обратите внимание: вызов await приостанавливает текущую задачу, но не блокирует поток. Система сама переключает контекст, когда результат будет готов, что делает код эффективным и безопасным.
2. Акторы (Actors) — безопасная работа с состоянием
Асинхронность добавляет проблему гонки данных (data race). Для её решения в Swift введены акторы. Актор — это ссылочный тип, который изолирует своё состояние и гарантирует, что только одна задача может обращаться к его изменяемым свойствам в любой момент времени.
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
// Использование актора
let counter = Counter()
Task {
await counter.increment()
let currentValue = await counter.getValue()
print("Текущее значение: \(currentValue)")
}
Ключевое слово actor автоматически делает все методы и свойства (кроме неизменяемых) асинхронными для внешнего доступа. Это позволяет избежать блокировок и взаимных блокировок (deadlocks), характерных для ручного управления блокировками (NSLock, DispatchSemaphore).
3. TaskGroup и параллельное выполнение задач
Когда необходимо выполнить несколько независимых асинхронных операций параллельно, используйте TaskGroup. Это структура, которая позволяет запускать дочерние задачи и собирать их результаты.
func fetchMultipleUsers(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
var users: [User] = []
for id in ids {
group.addTask {
let url = URL(string: "https://api.example.com/users/\(id)")!
let data = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data.0)
}
}
for try await user in group {
users.append(user)
}
return users
}
}
// Использование
Task {
let users = try? await fetchMultipleUsers(ids: [1, 2, 3, 4, 5])
print("Загружено пользователей: \(users?.count ?? 0)")
}
Метод withThrowingTaskGroup создаёт группу, которая может содержать задачи, способные выбрасывать ошибки. Если хотя бы одна задача завершится с ошибкой, вся группа упадёт с той же ошибкой. Для задач без ошибок используйте withTaskGroup.
Важный нюанс: порядок обработки результатов в цикле for await не гарантируется — задачи могут завершаться в произвольном порядке. Если важен порядок, сохраняйте идентификаторы и сортируйте после сбора.
4. Sendable — безопасная передача данных между задачами
С появлением акторов и асинхронности возникла необходимость контролировать, какие типы можно безопасно передавать между изолированными контекстами. Протокол Sendable — это маркер, который указывает, что значение типа можно безопасно использовать в конкурентной среде.
struct User: Sendable {
let id: Int
let name: String
}
// Класс должен быть финальным и содержать только Sendable-свойства
final class Configuration: @unchecked Sendable {
let apiURL: URL
init(apiURL: URL) {
self.apiURL = apiURL
}
}
// Пример с актором
actor DataManager {
private var cache: [Int: User] = [:]
func store(user: User) {
cache[user.id] = user
}
}
Большинство стандартных типов Swift (Int, String, Array, Dictionary, Optional) уже соответствуют Sendable. Для пользовательских структур это происходит автоматически, если все их свойства также Sendable. Для классов требуется явное указание @unchecked Sendable (с ответственностью разработчика) или строгое соблюдение правил изоляции.
Заключение
Асинхронное программирование в Swift — это не просто новая синтаксическая возможность, а фундаментальное изменение подхода к конкурентности. Ключевые концепции, которые мы рассмотрели:
- async/await — линейный и читаемый асинхронный код;
- Акторы — безопасное управление разделяемым состоянием;
- TaskGroup — эффективный параллелизм для независимых задач;
- Sendable — контроль безопасности данных при передаче.
Освоив эти инструменты, вы сможете писать более надёжные, производительные и поддерживаемые приложения для iOS и macOS. Советуем начинать с малого: переписать один колбэк на async/await, затем добавить актор для общего состояния, и постепенно внедрять TaskGroup для сетевых запросов. Практика — лучший способ закрепить знания. Успешной разработки!