Введение в асинхронное программирование в Swift
Современные мобильные приложения редко обходятся без сетевых запросов, работы с базой данных или обработки больших файлов. Все эти операции требуют времени, и если выполнять их в основном потоке, интерфейс приложения "зависнет", а пользователь получит неприятный опыт. Именно здесь на помощь приходит асинхронное программирование.
До появления Swift 5.5 разработчики использовали замыкания (completion handlers) или сторонние библиотеки (Combine, RxSwift), что часто приводило к "аду колбэков" и сложному для чтения коду. С выходом новой модели Swift Concurrency (async/await) написание асинхронного кода стало таким же простым и линейным, как синхронного.
В этом руководстве мы разберем ключевые концепции асинхронного программирования в Swift на практических примерах. Вы узнаете, как использовать async/await, Task, Actor и другие инструменты для создания быстрых и отзывчивых приложений под iOS и macOS.
Основные концепции Swift Concurrency
1. Async и Await: основа асинхронности
Ключевые слова async и await — это сердце новой модели. async объявляет функцию как асинхронную, а await приостанавливает выполнение до получения результата, не блокируя при этом поток.
Рассмотрим простой пример: загрузка данных из сети.
import Foundation
// Объявляем асинхронную функцию
func fetchUserData() async throws -> String {
let url = URL(string: "https://api.example.com/user/1")!
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? "Ошибка"
}
// Вызов асинхронной функции
Task {
do {
let userData = try await fetchUserData()
print("Данные пользователя: \(userData)")
} catch {
print("Ошибка загрузки: \(error.localizedDescription)")
}
}
Обратите внимание: try await означает, что мы ожидаем результат, который может выбросить ошибку. Код выглядит как обычный синхронный, но при этом не блокирует главный поток.
2. Task: точка входа в асинхронный мир
Асинхронные функции можно вызывать только из другого асинхронного контекста. Для запуска асинхронной операции из синхронного кода (например, из viewDidLoad()) используется Task.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Запускаем асинхронную задачу
Task {
let data = await loadData()
updateUI(with: data)
}
}
func loadData() async -> String {
// Имитация долгой загрузки
try? await Task.sleep(nanoseconds: 2_000_000_000)
return "Загруженные данные"
}
func updateUI(with data: String) {
print("Обновляем UI: \(data)")
}
}
Важно: Task создает новую асинхронную задачу, которая выполняется в фоновом потоке. Для обновления UI обязательно возвращайтесь на главный поток — по умолчанию await это делает автоматически.
3. Actor: защита от гонки данных
При работе с многопоточностью часто возникает проблема гонки данных (data race), когда несколько потоков одновременно изменяют одну переменную. Swift предлагает элегантное решение — Actor.
Actor — это ссылочный тип, который гарантирует, что только один поток может получить доступ к его изменяемому состоянию в любой момент времени.
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 требуют await, так как они могут выполняться в другом потоке. Это обеспечивает безопасность данных на уровне компилятора.
Продвинутые техники асинхронного программирования
4. AsyncSequence: работа с потоками данных
Иногда данные поступают не единым блоком, а последовательно — например, при чтении файла построчно или получении уведомлений. Для таких случаев Swift предоставляет протокол AsyncSequence.
import Foundation
// Асинхронная последовательность для чтения строк из файла
func readLines(from url: URL) -> AsyncStream<String> {
AsyncStream { continuation in
Task {
guard let fileHandle = try? FileHandle(forReadingFrom: url) else {
continuation.finish()
return
}
defer {
try? fileHandle.close()
}
for try await line in fileHandle.bytes.lines {
continuation.yield(line)
}
continuation.finish()
}
}
}
// Использование
Task {
let fileURL = Bundle.main.url(forResource: "data", withExtension: "txt")!
for await line in readLines(from: fileURL) {
print("Строка: \(line)")
}
}
AsyncStream позволяет создавать собственные асинхронные последовательности, а for await in — удобно их обрабатывать.
5. TaskGroup: параллельное выполнение задач
Когда нужно выполнить несколько независимых асинхронных операций одновременно, используйте TaskGroup. Это ускоряет работу в разы.
func loadAllImages(urls: [URL]) async throws -> [Data] {
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
var results: [Data] = []
for try await data in group {
results.append(data)
}
return results
}
}
// Использование
Task {
let urls = [URL(string: "https://example.com/image1.png")!,
URL(string: "https://example.com/image2.png")!]
do {
let images = try await loadAllImages(urls: urls)
print("Загружено \(images.count) изображений")
} catch {
print("Ошибка загрузки: \(error)")
}
}
Все задачи внутри группы выполняются параллельно, а результаты собираются по мере завершения.