Swift Concurrency: Пълно ръководство за async/await, актьори и структурирана конкурентност

Пълно ръководство за Swift Concurrency — от async/await и актьори, през Sendable и TaskGroup, до най-новите подобрения в Swift 6.2 Approachable Concurrency. С практически примери и добри практики.

Въведение: Защо конкурентността е толкова важна в iOS разработката

Съвременните мобилни приложения трябва да жонглират с купища задачи едновременно — зареждане на данни от мрежата, обработка на изображения, актуализиране на UI-а, работа с бази данни. Потребителите очакват приложенията да бъдат отзивчиви и плавни, без замръзвания или забавяния.

Именно тук конкурентността (concurrency) влиза в играта.

В продължение на години ние, iOS разработчиците, използвахме различни механизми за управление на конкурентни операции. Grand Central Dispatch (GCD) и DispatchQueue бяха основните ни инструменти, предоставяйки мощни възможности чрез опашки и блокове. Operation и OperationQueue добавиха допълнителна функционалност като зависимости между операции и отмяна. Въпреки това, нека бъдем честни — тези API-та имаха доста сериозни недостатъци:

  • Сложност при работа с вложени closure-и и callback-и (прословутият “callback hell”)
  • Трудно управление на грешки в асинхронен контекст
  • Липса на компилаторна проверка за data races (състояние на надпревара за данни)
  • Неясна отговорност за жизнения цикъл на асинхронните задачи
  • Проблеми с retain cycles и memory leaks

Swift 5.5, представен през 2021 г., донесе революционна промяна с въвеждането на модерния Swift Concurrency модел. Този модел включва async/await синтаксис, структурирана конкурентност, актьори (actors) и компилаторни проверки за безопасност на данните. Swift 6 (2024 г.) направи проверките за data race-ове задължителни по подразбиране, а Swift 6.2 (средата на 2025 г.) въведе “Approachable Concurrency” подобрения, които правят модела още по-интуитивен.

Основи на async/await

Ключовите думи async и await са сърцето на съвременната Swift конкурентност. Функция, маркирана като async, може да бъде суспендирана (временно спряна), за да изчака завършването на асинхронна операция, без да блокира текущата нишка. Това позволява на системата да използва ресурсите ефективно, изпълнявайки друга работа докато чака.

Дефиниране на асинхронни функции

Хайде да видим как се дефинира асинхронна функция:

// Асинхронна функция, която изтегля данни от URL
func fetchData(from url: URL) async throws -> Data {
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw NetworkError.invalidResponse
    }

    return data
}

enum NetworkError: Error {
    case invalidResponse
    case decodingFailed
}

Функцията е маркирана с async throws, което означава, че може да бъде суспендирана и може да хвърли грешка. Вътре използваме await, за да изчакаме асинхронната операция URLSession.shared.data(from:). Нищо сложно.

Извикване на асинхронни функции

Асинхронна функция може да бъде извикана само от друга асинхронна функция или в рамките на Task:

// Извикване в асинхронен контекст
func loadUserProfile(userId: String) async throws -> UserProfile {
    let url = URL(string: "https://api.example.com/users/\(userId)")!
    let data = try await fetchData(from: url)

    let decoder = JSONDecoder()
    let profile = try decoder.decode(UserProfile.self, from: data)

    return profile
}

// Извикване от синхронен контекст чрез Task
func displayUserProfile(userId: String) {
    Task {
        do {
            let profile = try await loadUserProfile(userId: userId)
            // Актуализиране на UI с профила
            print("Зареден профил: \(profile.name)")
        } catch {
            print("Грешка при зареждане: \(error)")
        }
    }
}

Точки на суспендиране

Всяко извикване с await е потенциална точка на суспендиране. Когато функцията достигне await, тя може да бъде суспендирана, освобождавайки нишката за друга работа.

След като асинхронната операция завърши, функцията продължава изпълнението си. Тук идва важният момент — състоянието може да се промени, докато функцията е суспендирана (и честно казано, това е една от най-честите причини за трудни за намиране бъгове):

class DataManager {
    var cachedData: [String: Data] = [:]

    func getData(for key: String) async throws -> Data {
        // Проверяваме кеша
        if let cached = cachedData[key] {
            return cached
        }

        // Точка на суспендиране - състоянието може да се промени тук!
        let data = try await fetchData(from: URL(string: "https://api.example.com/\(key)")!)

        // След суспендирането, cachedData може вече да съдържа този ключ
        // (ако друга задача го е добавила междувременно)
        cachedData[key] = data
        return data
    }
}

Task и TaskGroup: Структурирана конкурентност

Swift Concurrency въвежда концепцията за структурирана конкурентност, където всяка асинхронна задача има ясен жизнен цикъл и йерархична структура. Това елиминира проблеми като “забравени” задачи или неконтролирано разпространение на грешки.

Създаване на задачи с Task

Има няколко начина за създаване на задачи:

// 1. Неструктурирана задача (unstructured task)
Task {
    let data = try await fetchData(from: someURL)
    await processData(data)
}

// 2. Откачена задача (detached task) - не наследява контекст
Task.detached {
    // Изпълнява се независимо от родителския контекст
    await performBackgroundWork()
}

// 3. Задача с приоритет
Task(priority: .high) {
    await urgentOperation()
}

// 4. Съхраняване на референция за отмяна
let task = Task {
    await longRunningOperation()
}

// По-късно можем да отменим задачата
task.cancel()

async let: Паралелно изпълнение

async let позволява лесно паралелно изпълнение на множество асинхронни операции. Според мен това е една от най-елегантните функционалности в целия модел:

func loadDashboard() async throws -> Dashboard {
    // Трите заявки се изпълняват паралелно
    async let userProfile = fetchUserProfile()
    async let notifications = fetchNotifications()
    async let stats = fetchStatistics()

    // Изчакваме всички да завършат
    return try await Dashboard(
        profile: userProfile,
        notifications: notifications,
        stats: stats
    )
}

Ключовата разлика спрямо последователното извикване е, че всички три операции стартират почти едновременно. Ако една хвърли грешка, останалите автоматично се отменят.

TaskGroup: Динамични паралелни операции

За динамичен брой паралелни задачи използваме withTaskGroup:

func downloadImages(urls: [URL]) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
        // Добавяме задача за всеки URL
        for (index, url) in urls.enumerated() {
            group.addTask {
                let data = try await fetchData(from: url)
                guard let image = UIImage(data: data) else {
                    throw ImageError.invalidData
                }
                return (index, image)
            }
        }

        // Събираме резултатите
        var images: [UIImage?] = Array(repeating: nil, count: urls.count)
        for try await (index, image) in group {
            images[index] = image
        }

        return images.compactMap { $0 }
    }
}

enum ImageError: Error {
    case invalidData
}

TaskGroup осигурява структурирана конкурентност: scope-ът не завършва, докато всички дъщерни задачи не приключат или не бъдат отменени.

Отмяна на задачи

Отмяната е кооперативна в Swift Concurrency — задачите трябва сами да проверяват дали са отменени. Важно е да се разбере, че системата няма да спре задачата вместо вас:

func processLargeDataset(items: [DataItem]) async throws {
    for item in items {
        // Проверка за отмяна
        try Task.checkCancellation()

        // Или алтернативен начин
        guard !Task.isCancelled else {
            throw CancellationError()
        }

        await processItem(item)
    }
}

// Използване с отмяна
let task = Task {
    try await processLargeDataset(items: largeArray)
}

// След 5 секунди отменяме
Task {
    try await Task.sleep(nanoseconds: 5_000_000_000)
    task.cancel()
}

Актьори (Actors): Безопасност на данните

Един от най-значимите проблеми в конкурентното програмиране са data races — когато две или повече нишки достъпват едни и същи данни едновременно, и поне едната ги променя.

Актьорите решават този проблем чрез изолация на данните. Честно казано, това е може би най-елегантното решение, което съм виждал в съвременните езици за програмиране.

Какво представлява актьор

Актьорът е референтен тип (подобен на class), който защитава своето състояние като гарантира, че само една задача може да достъпва mutable състоянието в даден момент. Компилаторът налага тази безопасност вместо нас:

actor ImageCache {
    private var cache: [String: UIImage] = [:]
    private var loadingTasks: [String: Task<UIImage, Error>] = [:]

    func image(for key: String) -> UIImage? {
        return cache[key]
    }

    func cache(_ image: UIImage, for key: String) {
        cache[key] = image
    }

    func loadImage(from url: URL) async throws -> UIImage {
        let key = url.absoluteString

        // Проверяваме дали вече зареждаме това изображение
        if let existingTask = loadingTasks[key] {
            return try await existingTask.value
        }

        // Създаваме нова задача за зареждане
        let task = Task<UIImage, Error> {
            let data = try await fetchData(from: url)
            guard let image = UIImage(data: data) else {
                throw ImageError.invalidData
            }
            return image
        }

        loadingTasks[key] = task

        do {
            let image = try await task.value
            cache[key] = image
            loadingTasks[key] = nil
            return image
        } catch {
            loadingTasks[key] = nil
            throw error
        }
    }
}

Забележете, че достъпът до методите на актьора изисква await, защото може да се наложи да изчакаме актьорът да завърши текущата си работа:

let cache = ImageCache()

// Достъпът изисква await
Task {
    if let cachedImage = await cache.image(for: "profile") {
        // Използваме кешираното изображение
    } else {
        let image = try await cache.loadImage(from: profileURL)
        // Използваме новозареденото изображение
    }
}

@MainActor: Гарантиране на UI актуализации

UIKit и SwiftUI изискват всички UI актуализации да се случват на главната нишка. @MainActor е специален глобален актьор, който гарантира точно това:

@MainActor
class ProfileViewModel: ObservableObject {
    @Published var profile: UserProfile?
    @Published var isLoading = false
    @Published var errorMessage: String?

    private let networkService: NetworkService

    init(networkService: NetworkService) {
        self.networkService = networkService
    }

    func loadProfile(userId: String) async {
        isLoading = true
        errorMessage = nil

        do {
            // Мрежовата операция се изпълнява на background нишка
            let fetchedProfile = try await networkService.fetchProfile(userId: userId)

            // Присвояването се случва на MainActor автоматично
            profile = fetchedProfile
        } catch {
            errorMessage = "Грешка при зареждане: \(error.localizedDescription)"
        }

        isLoading = false
    }
}

Можем да маркираме и отделни методи или свойства:

class DataProcessor {
    private var results: [Result] = []

    // Този метод трябва да се изпълнява на главната нишка
    @MainActor
    func updateUI(with result: Result) {
        results.append(result)
        // Актуализиране на UI компоненти
    }

    // Този метод може да се изпълнява навсякъде
    func processData(_ data: Data) async -> Result {
        // Тежка обработка
        return Result(data: data)
    }
}

nonisolated: Синхронен достъп до актьор

Понякога имаме нужда от синхронен достъп до immutable данни в актьор. nonisolated позволява точно това:

actor GameState {
    private var score: Int = 0
    let gameId: String
    let configuration: GameConfiguration

    init(gameId: String, configuration: GameConfiguration) {
        self.gameId = gameId
        self.configuration = configuration
    }

    // Може да се достъпва синхронно, защото е immutable
    nonisolated var id: String {
        gameId
    }

    // Изисква await, защото променя състоянието
    func incrementScore() {
        score += 1
    }

    // Изисква await, защото чете mutable състояние
    func currentScore() -> Int {
        return score
    }
}

// Използване
let game = GameState(gameId: "game-123", configuration: config)

// Синхронен достъп - без await
let id = game.id

// Асинхронен достъп - с await
Task {
    let score = await game.currentScore()
}

Sendable протокол: Безопасно предаване между конкурентни контексти

Когато предаваме данни между различни актьори или задачи, трябва да гарантираме, че е безопасно да го правим. Sendable протоколът маркира типове, които могат безопасно да се предават през конкурентни граници.

Автоматично съответствие с Sendable

Някои типове автоматично съответстват на Sendable:

  • Value types (struct, enum), съставени само от Sendable типове
  • Актьори
  • Класове, маркирани с @MainActor
  • Класове, които са финални и съдържат само immutable Sendable свойства
// Автоматично Sendable - value type с Sendable полета
struct UserProfile: Sendable {
    let id: String
    let name: String
    let email: String
    let registrationDate: Date
}

// Автоматично Sendable - финален клас с immutable полета
final class Configuration: Sendable {
    let apiKey: String
    let baseURL: URL
    let timeout: TimeInterval

    init(apiKey: String, baseURL: URL, timeout: TimeInterval) {
        self.apiKey = apiKey
        self.baseURL = baseURL
        self.timeout = timeout
    }
}

Явно съответствие с Sendable

За по-сложни случаи можем ръчно да декларираме съответствие:

// Клас, който ръчно управлява синхронизацията
final class ThreadSafeCounter: @unchecked Sendable {
    private let lock = NSLock()
    private var value: Int = 0

    func increment() {
        lock.lock()
        defer { lock.unlock() }
        value += 1
    }

    func getValue() -> Int {
        lock.lock()
        defer { lock.unlock() }
        return value
    }
}

@unchecked Sendable казва на компилатора, че ние гарантираме безопасността, дори че той не може да я провери автоматично. Използвайте внимателно — грешка тук означава потенциален data race, който компилаторът няма да хване.

@Sendable closures

Closure-ите, които се предават на асинхронни API-та, често трябва да бъдат @Sendable:

func performAsync(operation: @Sendable @escaping () async -> Void) {
    Task {
        await operation()
    }
}

// Грешка: този closure не е Sendable защото capture-ва mutable променлива
var counter = 0
performAsync {
    counter += 1  // Компилаторна грешка!
}

// Правилно: capture-ваме immutable стойност
let userId = "user-123"
performAsync {
    await loadData(for: userId)  // OK
}

Swift 6: Строга проверка за конкурентност

Swift 6, представен през 2024 г., направи строгата проверка за конкурентност (strict concurrency checking) активна по подразбиране. На практика това означава, че компилаторът активно търси потенциални data races и отказва да компилира несигурен код. Сурово, но справедливо.

Проверки за изолация

Компилаторът проверява дали не се нарушава изолацията на актьорите:

actor DatabaseManager {
    private var cache: [String: Data] = [:]

    func getData(for key: String) -> Data? {
        return cache[key]
    }
}

let manager = DatabaseManager()

// Swift 6 грешка: достъп до актьор извън async контекст
// let data = manager.getData(for: "key")  // НЕ КОМПИЛИРА

// Правилно:
Task {
    let data = await manager.getData(for: "key")
}

Глобално изменяемо състояние

Swift 6 също така налага ограничения върху глобално mutable състояние. Между другото, това е едно от най-важните подобрения за безопасността на кода:

// Грешка в Swift 6: глобална mutable променлива не е Sendable
// var globalCounter = 0  // НЕ КОМПИЛИРА

// Решения:

// 1. Използване на актьор
actor GlobalState {
    static let shared = GlobalState()
    private var counter = 0

    func increment() {
        counter += 1
    }

    func getCounter() -> Int {
        return counter
    }
}

// 2. Immutable глобално състояние (OK)
let globalConfiguration = Configuration(apiKey: "key", baseURL: url, timeout: 30)

// 3. MainActor изолация
@MainActor
var sharedUIState = UIState()

Мигриране към Swift 6

За съществуващи проекти можете да активирате строгата проверка постепенно:

// В Build Settings активирайте "Strict Concurrency Checking"
// Или за конкретен файл:
// @preconcurrency import OldLibrary

// За типове от библиотеки без Sendable поддръжка:
extension LegacyType: @unchecked Sendable {}

Swift 6.2 “Approachable Concurrency”: Какво ново?

Swift 6.2, представен през средата на 2025 г., донесе значителни подобрения под наслов “Approachable Concurrency”. Тези промени правят модела по-интуитивен и по-близък до традиционния синхронен код.

Философията е ясна: прогресивно разкриване — Swift трябва да изисква от вас да разбирате толкова конкурентност, колкото реално използвате. Не повече.

nonisolated(nonsending): Изпълнение на caller’s executor

Един от най-важните въпроси в Swift Concurrency беше: на коя нишка/executor се изпълнява моят код? Swift 6.2 въвежда nonisolated(nonsending), което означава, че функцията се изпълнява на executor-а на извикващия код по подразбиране:

// Преди Swift 6.2: тази функция може да се изпълнява на различни нишки
func processData(_ data: Data) async -> ProcessedResult {
    // Неясно на коя нишка сме
    return ProcessedResult(data: data)
}

// Swift 6.2: явно указваме поведението
nonisolated(nonsending) func processDataOnCaller(_ data: Data) async -> ProcessedResult {
    // Изпълнява се на същия executor като caller-а
    return ProcessedResult(data: data)
}

// Използване:
@MainActor
func updateUI() async {
    let data = loadData()

    // Изпълнява се на MainActor, защото caller-ът е на MainActor
    let result = await processDataOnCaller(data)

    // Можем директно да актуализираме UI - все още сме на главната нишка
    displayResult(result)
}

Това е значително подобрение за разбираемост и производителност — спестява ненужното превключване на контекст.

@concurrent: Изрично паралелно изпълнение

Когато искаме функция изрично да се изпълнява паралелно на background нишка, използваме @concurrent. Този атрибут автоматично прави функцията nonisolated, така че не е нужно да пишете и двете:

@concurrent
func performHeavyComputation(_ input: LargeDataSet) async -> ComputationResult {
    // Този код ВИНАГИ се изпълнява на background thread pool
    var result = ComputationResult()

    for item in input.items {
        result.aggregate(compute(item))
    }

    return result
}

@MainActor
class ViewModel {
    func processLargeDataset() async {
        let dataset = loadDataset()

        // Тази операция се изпълнява на background нишка,
        // дори че сме в MainActor контекст
        let result = await performHeavyComputation(dataset)

        // Връщаме се на главната нишка за UI актуализация
        updateUI(with: result)
    }
}

Използвайте @concurrent, когато функцията изпълнява тежка CPU работа (като JSON декодиране или обработка на изображения), която не трябва да блокира актьора на извикващия.

Подразбираща се MainActor изолация

С флага -default-isolation MainActor в Swift 6.2, модулите могат да се изпълняват по подразбиране на главния актьор. Това означава, че не е нужно да пишете @MainActor навсякъде — кодът ви автоматично е изолиран към MainActor, освен ако изрично не посочите друго с @concurrent или nonisolated.

SE-0462: Детектиране на ескалация на приоритет

Swift 6.2 въвежда и възможност за следене на ескалация на приоритета на задачи чрез withTaskPriorityEscalationHandler. Когато високоприоритетна задача чака нископриоритетна, системата автоматично повишава приоритета:

func processData() async throws {
    try await withTaskPriorityEscalationHandler {
        // Основна работа тук
        await longRunningComputation()
    } onPriorityEscalated: { newPriority in
        // Извиква се когато приоритетът е повишен
        print("Приоритетът е ескалиран до: \(newPriority)")
    }
}

SE-0469: Имена на задачи за debugging

Задачите вече могат да имат имена, което значително улеснява debugging-а в Xcode Instruments. Ако някога сте се борили с анонимни задачи в дебъгера, ще оцените тази промяна:

Task(name: "LoadUserProfile") {
    let profile = try await networkService.fetchProfile(userId: userId)
    await updateProfile(profile)
}

Task(name: "ImageProcessing-\(imageId)") {
    let image = try await loadImage(id: imageId)
    let processed = await processImage(image)
    await cache.store(processed, for: imageId)
}

// В TaskGroup също можете да именувате задачи
try await withThrowingTaskGroup(of: Data.self) { group in
    group.addTask(name: "FetchPage-1") {
        try await fetchPage(1)
    }
    group.addTask(name: "FetchPage-2") {
        try await fetchPage(2)
    }
}

withTaskExecutorPreference: Контрол върху изпълнението

За фино управление на изпълнителния контекст използваме withTaskExecutorPreference. Той позволява задачата и всички нейни дъщерни задачи да се изпълняват предпочитано на конкретен executor:

// Задаване на предпочитание за executor
await withTaskExecutorPreference(customExecutor) {
    // Този код и всички дъщерни задачи ще се изпълняват
    // предпочитано на customExecutor
    await performWork()

    async let result1 = computePartA()  // Също на customExecutor
    async let result2 = computePartB()  // Също на customExecutor

    return await combine(result1, result2)
}

// Може да се използва и при създаване на Task
Task(executorPreference: computeExecutor) {
    await heavyComputation()
}

Практически шаблони: Примери от реалния свят

Нека разгледаме няколко практически шаблона, които вероятно ще ви потрябват в ежедневната работа.

Шаблон 1: Модерен ViewModel с MainActor

Пълноценен ViewModel с поддръжка за зареждане, отмяна и обработка на грешки:

@MainActor
final class ArticleListViewModel: ObservableObject {
    @Published private(set) var articles: [Article] = []
    @Published private(set) var isLoading = false
    @Published private(set) var errorMessage: String?

    private let repository: ArticleRepository
    private var loadTask: Task<Void, Never>?

    init(repository: ArticleRepository) {
        self.repository = repository
    }

    func loadArticles() {
        // Отменяме предишната задача ако има такава
        loadTask?.cancel()

        loadTask = Task { [weak self] in
            guard let self = self else { return }

            self.isLoading = true
            self.errorMessage = nil

            do {
                let fetchedArticles = try await self.repository.fetchArticles()

                guard !Task.isCancelled else { return }

                self.articles = fetchedArticles
            } catch {
                guard !Task.isCancelled else { return }
                self.errorMessage = error.localizedDescription
            }

            self.isLoading = false
        }
    }

    func refreshArticle(id: String) async {
        do {
            let updated = try await repository.fetchArticle(id: id)

            if let index = articles.firstIndex(where: { $0.id == id }) {
                articles[index] = updated
            }
        } catch {
            errorMessage = "Грешка при опресняване: \(error.localizedDescription)"
        }
    }

    deinit {
        loadTask?.cancel()
    }
}

Шаблон 2: Мрежов слой с актьори

Thread-safe мрежов слой с дедупликация на заявки (нещо, което вероятно ще ви трябва рано или късно):

actor NetworkManager {
    private let session: URLSession
    private var inFlightRequests: [URL: Task<Data, Error>] = [:]

    init(configuration: URLSessionConfiguration = .default) {
        self.session = URLSession(configuration: configuration)
    }

    func data(from url: URL) async throws -> Data {
        // Проверка дали вече изпълняваме този request
        if let existingTask = inFlightRequests[url] {
            return try await existingTask.value
        }

        let task = Task<Data, Error> {
            let (data, response) = try await session.data(from: url)

            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode) else {
                throw NetworkError.httpError(statusCode: 0)
            }

            return data
        }

        inFlightRequests[url] = task

        do {
            let data = try await task.value
            inFlightRequests[url] = nil
            return data
        } catch {
            inFlightRequests[url] = nil
            throw error
        }
    }

    func cancelAllRequests() {
        for task in inFlightRequests.values {
            task.cancel()
        }
        inFlightRequests.removeAll()
    }
}

Шаблон 3: Pipeline за зареждане на изображения

Комплексен pipeline с многослойно кеширане:

actor ImageLoader {
    private let networkManager: NetworkManager
    private var memoryCache: [URL: UIImage] = [:]

    init(networkManager: NetworkManager) {
        self.networkManager = networkManager
    }

    func loadImage(from url: URL) async throws -> UIImage {
        // Стъпка 1: Проверка в memory cache
        if let cached = memoryCache[url] {
            return cached
        }

        // Стъпка 2: Зареждане от мрежата
        let data = try await networkManager.data(from: url)

        guard let image = UIImage(data: data) else {
            throw ImageError.invalidData
        }

        // Стъпка 3: Кеширане
        memoryCache[url] = image

        return image
    }

    func clearCache() {
        memoryCache.removeAll()
    }
}

// SwiftUI интеграция
@MainActor
class AsyncImageViewModel: ObservableObject {
    @Published var image: UIImage?
    @Published var isLoading = false

    private let imageLoader: ImageLoader
    private var loadTask: Task<Void, Never>?

    init(imageLoader: ImageLoader) {
        self.imageLoader = imageLoader
    }

    func load(from url: URL) {
        loadTask?.cancel()

        loadTask = Task { [weak self] in
            guard let self = self else { return }
            self.isLoading = true

            do {
                let loaded = try await self.imageLoader.loadImage(from: url)
                guard !Task.isCancelled else { return }
                self.image = loaded
            } catch {
                guard !Task.isCancelled else { return }
                print("Грешка при зареждане: \(error)")
            }

            self.isLoading = false
        }
    }
}

Добри практики и често срещани грешки

Добри практики

1. Предпочитайте структурирана конкурентност пред неструктурирана

// Добре: структурирана конкурентност с ясен lifecycle
func processItems() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for item in items {
            group.addTask {
                try await processItem(item)
            }
        }
        try await group.waitForAll()
    }
}

// Избягвайте: неструктурирани задачи без контрол
func processItemsPoorly() {
    for item in items {
        Task {
            try? await processItem(item)
        }
    }
}

2. Винаги проверявайте за отмяна в дълги операции

func processLargeArray(_ items: [Item]) async throws -> [ProcessedItem] {
    var results: [ProcessedItem] = []

    for item in items {
        try Task.checkCancellation()
        let result = await process(item)
        results.append(result)
    }

    return results
}

3. Използвайте async let за независими паралелни операции

// Бавно: последователно изпълнение
func loadDashboard() async throws -> Dashboard {
    let user = try await fetchUser()
    let posts = try await fetchPosts()
    let stats = try await fetchStats()
    return Dashboard(user: user, posts: posts, stats: stats)
}

// Бързо: паралелно изпълнение
func loadDashboard() async throws -> Dashboard {
    async let user = fetchUser()
    async let posts = fetchPosts()
    async let stats = fetchStats()
    return try await Dashboard(user: user, posts: posts, stats: stats)
}

4. Използвайте @MainActor за целия ViewModel

// Маркирайте целия клас вместо отделни методи
@MainActor
class ViewModel: ObservableObject {
    @Published var data: [Item] = []

    func loadData() async {
        data = try? await fetchItems()
    }
}

Често срещани грешки

Сега нека разгледаме грешките, които повечето от нас са правили поне веднъж.

1. Блокиране на MainActor с тежки операции

// ЛОШО: тежка операция на главната нишка
@MainActor
class BadViewModel {
    func process() async {
        let result = performExpensiveComputation()  // Блокира UI!
        updateUI(with: result)
    }
}

// Правилно: използвайте @concurrent или Task.detached
@MainActor
class GoodViewModel {
    func process() async {
        let result = await Task.detached {
            return performExpensiveComputation()
        }.value
        updateUI(with: result)
    }
}

2. Неправилна обработка на грешки в TaskGroup

// ЛОШО: грешките се поглъщат
func processAll() async {
    await withTaskGroup(of: Void.self) { group in
        for item in items {
            group.addTask {
                try? await process(item)  // Грешките се игнорират!
            }
        }
    }
}

// Добре: използвайте withThrowingTaskGroup
func processAll() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for item in items {
            group.addTask {
                try await process(item)
            }
        }
        try await group.waitForAll()
    }
}

3. Създаване на data races с @unchecked Sendable

// ОПАСНО: това НЕ е thread-safe!
class UnsafeCache: @unchecked Sendable {
    var storage: [String: Data] = [:]  // Няма синхронизация!
}

// Правилно: използвайте актьор
actor SafeCache {
    var storage: [String: Data] = [:]
}

4. Пренебрегване на Task lifecycle в SwiftUI

// ЛОШО: задачата продължава и след като view-то е унищожено
struct BadView: View {
    var body: some View {
        Text("Hello")
            .onAppear {
                Task {
                    await loadData()  // Няма отмяна!
                }
            }
    }
}

// Добре: .task автоматично отменя при disappear
struct GoodView: View {
    var body: some View {
        Text("Hello")
            .task {
                await loadData()  // Автоматична отмяна
            }
    }
}

Заключение

Swift Concurrency представлява фундаментална промяна в начина, по който пишем асинхронен код. Преходът от GCD и callback-базирани API-та към async/await, актьори и структурирана конкурентност донесе множество предимства:

  • Безопасност: Компилаторните проверки елиминират data races още на етап компилация
  • Четливост: Асинхронният код изглежда и се чете почти като синхронен
  • Поддръжка: Структурираната конкурентност прави жизнения цикъл на задачите ясен и предвидим
  • Производителност: Ефективното използване на нишки и кооперативното отменяне подобряват производителността
  • Интеграция: Отличната интеграция със SwiftUI и останалите Apple фреймуърки опростява архитектурата

Swift 6 и особено Swift 6.2 с “Approachable Concurrency” подобренията направиха модела още по-достъпен. Ключовите нововъведения — nonisolated(nonsending), @concurrent, подразбиращата се MainActor изолация и именуване на задачи — дават ясен контрол върху изпълнителния контекст и значително улесняват разработката и debugging-а.

Ключът към успешното използване на Swift Concurrency е разбирането на основните концепции. Как работят точките на суспендиране. Какво представлява actor изолацията. Защо Sendable е важен. И как структурираната конкурентност гарантира безопасност.

С тези знания и добрите практики, които разгледахме, можете спокойно да изградите бързи, безопасни и поддържаеми iOS приложения. А не забравяйте и да комбинирате Swift Concurrency с други модерни технологии — SwiftData за persistence, SwiftUI за декларативен UI и Observation framework за реактивно програмиране. Заедно тези инструменти ви дават всичко необходимо, за да създавате страхотни приложения за Apple платформите.