Súbežnosť v Swift 6.2: Async/await, aktéri a štruktúrovaná súbežnosť v praxi

Praktický sprievodca modernou súbežnosťou v Swift 6.2. Od async/await cez aktérov a TaskGroup po novinky ako predvolená MainActor izolácia a @concurrent — všetko s príkladmi kódu.

Súbežnosť v Swift 6.2: Prečo je to téma, ktorej sa oplatí venovať

Súbežnosť — to je jedno z tých slov, pri ktorých mnohí vývojári najprv zdvihnú obočie a potom radšej rýchlo pretočia stránku. Ale úprimne? Ak robíte čokoľvek v Swift, bez poriadneho pochopenia concurrency modelu sa ďaleko nedostanete. Moderné aplikácie jednoducho musia robiť viac vecí naraz — sťahovať dáta, spracovávať obrázky, aktualizovať UI — a to všetko bez toho, aby sa používateľovi zaseklo rozhranie.

Swift prešiel v tejto oblasti naozaj dlhú cestu. Od čias Grand Central Dispatch a tých nekonečných completion handlerov sme sa dostali k async/await a celému modernému concurrency modelu. A s príchodom Swift 6.2 to ide ešte ďalej — nové funkcie ako „Approachable Concurrency" robia súbežné programovanie prístupnejším aj pre tých, ktorí sa mu doteraz vyhýbali.

V tomto článku si prejdeme všetko podstatné. Od základov async/await cez štruktúrovanú súbežnosť, aktérov (actors) až po novinky v Swift 6.2. Každý koncept doplním praktickými príkladmi, ktoré si môžete hneď vyskúšať.

Ako to bolo predtým (a prečo to nebolo ideálne)

Než sa pustíme do moderného prístupu, hodí sa pripomenúť, ako sme to robili „po starom". Ak ste niekedy pracovali s completion handlermi, viete o čom hovorím.

func stahnuDataStarymSposobom(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }

        guard let data = data else {
            completion(.failure(NSError(domain: "ChybaData", code: -1)))
            return
        }

        completion(.success(data))
    }.resume()
}

// Použitie s vnoreným callbackom
stahnuDataStarymSposobom(url: someURL) { result in
    switch result {
    case .success(let data):
        print("Získané dáta: \(data.count) bajtov")
    case .failure(let error):
        print("Chyba: \(error)")
    }
}

Funguje to? Áno. Je to pekné? Nie veľmi.

Kód je ťažko čitateľný, náchylný na memory leaky (tie zachytené referencie v closure vedia poriadne potrápiť) a keď potrebujete kombinovať viacero asynchrónnych volaní za sebou, rýchlo sa dostanete do neslávne známeho „callback hell". Swift 6.2 toto všetko rieši oveľa elegantnejšie.

Moderný concurrency model prináša typovú bezpečnosť, ochranu pred data races, jednoduchšiu syntax a — čo je podľa mňa najdôležitejšie — kompilátorové kontroly, ktoré vám mnohé chyby chytia ešte pred spustením kódu. Swift využíva kooperatívne vláknové prepínanie, takže systém zvládne tisícky súbežných úloh bez zbytočného overhead-u.

Základy async/await

Takže, poďme na to. Kľúčové slová async a await sú základom modernej súbežnosti v Swift. Funkcia označená ako async sa môže pozastaviť bez blokovania vlákna — systém medzitým robí inú prácu. A await jednoducho označuje miesto, kde k tomu pozastaveniu môže dôjsť.

Prepíšme ten predchádzajúci príklad:

func stahnuData(z 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 URLError(.badServerResponse)
    }

    return data
}

// Použitie je oveľa prirodzenejšie
Task {
    do {
        let data = try await stahnuData(z: someURL)
        print("Získané dáta: \(data.count) bajtov")
    } catch {
        print("Chyba: \(error)")
    }
}

Vidíte ten rozdiel? Kód vyzerá skoro ako synchronný. Žiadne vnorené callbacky, žiadne completiony. Kľúčové slovo await jasne hovorí „tu sa môže niečo pozastaviť" a throws umožňuje klasický try-catch namiesto Result typu v closure.

Poďme na niečo praktickejšie — načítanie a dekódovanie JSON z API:

struct Uzivatel: Codable {
    let id: Int
    let meno: String
    let email: String
}

func ziskajUzivatela(id: Int) async throws -> Uzivatel {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    let uzivatel = try JSONDecoder().decode(Uzivatel.self, from: data)
    return uzivatel
}

func ziskajViacerychUzivatelov(ids: [Int]) async throws -> [Uzivatel] {
    var uzivatelia: [Uzivatel] = []

    for id in ids {
        let uzivatel = try await ziskajUzivatela(id: id)
        uzivatelia.append(uzivatel)
    }

    return uzivatelia
}

Toto je síce čistý a prehľadný kód, ale nie je optimálny — každého užívateľa sťahujeme sekvenčne, jedného po druhom. Čo keď ich chceme sťahovať všetkých naraz? Na to nám poslúži štruktúrovaná súbežnosť.

Štruktúrovaná súbežnosť: Async let a TaskGroup

Štruktúrovaná súbežnosť je (trochu krkolomný) názov pre jednoduchý koncept — všetky asynchrónne úlohy majú jasný začiatok, koniec a patria do nejakej štruktúry. Nič vám „neutečie". Swift to implementuje cez async let a TaskGroup.

Async let — jednoduché paralelné volania

Pomocou async let spustíte operáciu, ktorá beží súbežne s ostatným kódom. Výsledok si vyzdvihnete, keď ho budete potrebovať:

func stahnuObrazokAMetadata(url: URL) async throws -> (UIImage, Data) {
    // Spustíme obe operácie súbežne
    async let obrazokData = stahnuData(z: url)
    async let metadata = stahnuMetadata(z: url)

    // Počkáme na obe operácie
    let (imgData, meta) = try await (obrazokData, metadata)

    guard let image = UIImage(data: imgData) else {
        throw NSError(domain: "ChybaObrazka", code: -1)
    }

    return (image, meta)
}

func stahnuMetadata(z url: URL) async throws -> Data {
    let metadataURL = url.appendingPathComponent("metadata.json")
    let (data, _) = try await URLSession.shared.data(from: metadataURL)
    return data
}

Obrázok aj metadata sa sťahujú naraz — žiadne čakanie na jedno, kým sa dokončí druhé. A bonus? Ak jedna operácia zlyhá, tá druhá sa automaticky zruší. O to sa nemusíte starať.

TaskGroup — keď nevieme koľko úloh bude

Pre dynamický počet súbežných operácií tu máme TaskGroup. Povedzme, že máte zoznam URL adries a chcete stiahnuť všetky obrázky paralelne:

func stahnuVsetkyObrazky(urls: [URL]) async throws -> [UIImage] {
    try await withThrowingTaskGroup(of: (Int, UIImage).self) { skupina in
        // Vytvoríme úlohu pre každú URL
        for (index, url) in urls.enumerated() {
            skupina.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let image = UIImage(data: data) else {
                    throw NSError(domain: "ChybaObrazka", code: -1)
                }
                return (index, image)
            }
        }

        // Zozbierame výsledky v správnom poradí
        var obrazky: [UIImage?] = Array(repeating: nil, count: urls.count)
        for try await (index, obrazok) in skupina {
            obrazky[index] = obrazok
        }

        return obrazky.compactMap { $0 }
    }
}

Všetky obrázky sa sťahujú paralelne a withThrowingTaskGroup zabezpečí, že sa funkcia nevráti, kým nie sú všetky úlohy dokončené. Ak niektorá zlyhá, zvyšok sa zruší.

A čo keď chcete obrázky zobrazovať priebežne, hneď ako prídu? Žiaden problém:

func spracujObrazkyPriebezne(urls: [URL]) async throws {
    try await withThrowingTaskGroup(of: UIImage.self) { skupina in
        for url in urls {
            skupina.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                guard let image = UIImage(data: data) else {
                    throw NSError(domain: "ChybaObrazka", code: -1)
                }
                return image
            }
        }

        // Spracujeme každý obrázok hneď ako je dostupný
        for try await obrazok in skupina {
            await zobrazObrazok(obrazok)
        }
    }
}

@MainActor
func zobrazObrazok(_ obrazok: UIImage) {
    // Aktualizácia UI - musí byť na main threade
    print("Zobrazujem obrázok veľkosti: \(obrazok.size)")
}

Aktéri: Koniec data races

Aktéri (actors) sú podľa mňa asi najvýznamnejší prínos celého Swift concurrency modelu. Problém je jednoduchý — keď viacero vlákien pristupuje k rovnakým dátam a aspoň jedno z nich zapisuje, máte data race. A data races sú nočná mora na debugovanie.

Aktér je niečo ako trieda, ale s jedným zásadným rozdielom — automaticky zabezpečí, že k jeho stavu pristupuje vždy len jeden kúsok kódu naraz.

actor UcetManager {
    private var zostatok: Double = 0.0
    private var transakcie: [String] = []

    func getZostatok() -> Double {
        return zostatok
    }

    func vklad(suma: Double) {
        zostatok += suma
        transakcie.append("Vklad: \(suma)")
    }

    func vyber(suma: Double) throws {
        guard zostatok >= suma else {
            throw NSError(domain: "NedostatokProstriedkov", code: -1)
        }
        zostatok -= suma
        transakcie.append("Výber: \(suma)")
    }

    func getHistoriu() -> [String] {
        return transakcie
    }
}

Keď voláte metódy aktéra zvonka, musíte použiť await — to je ten signál, že možno budete musieť chvíľu počkať, kým sa aktér „uvoľní":

let ucet = UcetManager()

Task {
    await ucet.vklad(suma: 1000)
    print("Zostatok: \(await ucet.getZostatok())")

    do {
        try await ucet.vyber(suma: 500)
        print("Výber úspešný. Nový zostatok: \(await ucet.getZostatok())")
    } catch {
        print("Chyba pri výbere: \(error)")
    }
}

Žiadne locky, žiadne semafory, žiadne ručné synchronizovanie. Aktér to rieši za vás.

Nonisolated metódy — keď izolácia nie je potrebná

Nie všetky metódy aktéra potrebujú prístup k jeho meniteľnému stavu. Takéto metódy môžete označiť ako nonisolated a volať ich bez await:

actor DataCache {
    private var cache: [String: Data] = [:]
    private let maxVelkost: Int

    init(maxVelkost: Int = 100) {
        self.maxVelkost = maxVelkost
    }

    func uloz(kluc: String, data: Data) {
        if cache.count >= maxVelkost {
            if let prvaPolozka = cache.first {
                cache.removeValue(forKey: prvaPolozka.key)
            }
        }
        cache[kluc] = data
    }

    func ziskaj(kluc: String) -> Data? {
        return cache[kluc]
    }

    nonisolated func vytvorKluc(pre url: URL) -> String {
        // Táto metóda nepristupuje k stavu aktéra
        return url.absoluteString
            .addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? ""
    }
}

let cache = DataCache()

// Môžeme volať bez await
let kluc = cache.vytvorKluc(pre: someURL)

// Ale prístup k cache vyžaduje await
Task {
    await cache.uloz(kluc: kluc, data: someData)
}

MainActor — váš najlepší kamarát pre UI

Každý iOS vývojár to pozná — aktualizácia UI musí byť na main threade. MainActor je špeciálny globálny aktér, ktorý práve toto zabezpečuje. Označíte ním triedu a máte pokoj:

@MainActor
class ObrazokViewModel: ObservableObject {
    @Published var obrazok: UIImage?
    @Published var stav: String = "Čakám..."

    func stahnuObrazok(z url: URL) async {
        stav = "Sťahujem..."

        do {
            let (data, _) = try await URLSession.shared.data(from: url)

            // Spracovanie na pozadí
            let spracovanyObrazok = await spracujObrazokNaPozadi(data: data)

            // Aktualizácia @Published vlastností je automaticky na MainActor
            self.obrazok = spracovanyObrazok
            self.stav = "Hotovo"
        } catch {
            self.stav = "Chyba: \(error.localizedDescription)"
        }
    }

    nonisolated func spracujObrazokNaPozadi(data: Data) async -> UIImage? {
        return await Task.detached {
            guard let obrazok = UIImage(data: data) else { return nil }
            // Náročné spracovanie mimo hlavné vlákno
            return obrazok
        }.value
    }
}

Protokol Sendable: Bezpečný prenos dát medzi vláknami

Sendable je marker protokol, ktorý hovorí kompilátoru: „Tento typ je bezpečné posielať medzi súbežnými doménami." Znie to jednoducho, ale je to kľúčové pre bezpečnosť celého systému.

Základné typy (Int, String, Bool...) sú Sendable automaticky. Pre vlastné typy to závisí od situácie:

// Štruktúry s iba Sendable vlastnosťami sú automaticky Sendable
struct Pouzivatel: Sendable {
    let id: Int
    let meno: String
    let email: String
}

// Triedy musia byť final a immutable
final class KonfiguraciaAPI: Sendable {
    let baseURL: URL
    let apiKluc: String
    let timeout: TimeInterval

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

Čo ale keď potrebujete meniteľný stav? Máte viacero možností:

// Riešenie 1: Použitie aktéra (najčistejšie)
actor PocitadloManager {
    private var pocitadlo: Int = 0

    func increment() {
        pocitadlo += 1
    }

    func getValue() -> Int {
        return pocitadlo
    }
}

// Riešenie 2: @unchecked Sendable s manuálnou synchronizáciou
final class ThreadSafePocitadlo: @unchecked Sendable {
    private var pocitadlo: Int = 0
    private let lock = NSLock()

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

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

// Riešenie 3: OSAllocatedUnfairLock (od Swift 5.9+)
import os

final class ModernePocitadlo: @unchecked Sendable {
    private let stav = OSAllocatedUnfairLock(initialState: 0)

    func increment() {
        stav.withLock { $0 += 1 }
    }

    func getValue() -> Int {
        stav.withLock { $0 }
    }
}

Osobne odporúčam aktérov vždy, keď je to možné. Sú najčistejšie a kompilátor vám pomáha. @unchecked Sendable je únikový východ — používajte ho len keď naozaj viete čo robíte.

@Sendable closures

Closures posielané medzi súbežnými doménami tiež musia byť označené ako @Sendable:

func vykonajAsyncOperaciu(
    completion: @Sendable @escaping (Result<String, Error>) -> Void
) {
    Task {
        do {
            let vysledok = try await nejakeAsyncVypocet()
            completion(.success(vysledok))
        } catch {
            completion(.failure(error))
        }
    }
}

func nejakeAsyncVypocet() async throws -> String {
    try await Task.sleep(nanoseconds: 1_000_000_000)
    return "Hotovo"
}

Novinky v Swift 6.2: Approachable Concurrency

Teraz sa dostávame k tomu naozaj zaujímavému. Swift 6.2 prináša iniciatívu „Approachable Concurrency", ktorej cieľom je spraviť súbežné programovanie prístupnejším. A úprimne — bolo to potrebné. Concurrency model v Swift 5.x a 6.0 bol síce silný, ale pre mnohých vývojárov (zvlášť začiatočníkov) dosť náročný na pochopenie.

Predvolená MainActor izolácia

Toto je asi najväčšia zmena. V Swift 6.2 si môžete na úrovni modulu zapnúť predvolenú MainActor izoláciu. Čo to znamená v praxi? Takmer všetok váš kód automaticky beží na main threade, pokiaľ neurčíte inak.

Pre aplikačný kód je to geniálne — väčšina kódu v typickej iOS appke aj tak súvisí s UI:

// V Swift 6.2 s predvolenou MainActor izoláciou
// Táto trieda je automaticky @MainActor
class ProfilViewController: UIViewController {
    var uzivatel: Pouzivatel?

    override func viewDidLoad() {
        super.viewDidLoad()
        nacitajProfil()
    }

    func nacitajProfil() {
        Task {
            do {
                let uzivatel = try await ziskajUzivatela(id: 123)
                self.uzivatel = uzivatel
                aktualizujUI()
            } catch {
                zobrazChybu(error)
            }
        }
    }

    func aktualizujUI() {
        guard let uzivatel = uzivatel else { return }
        title = uzivatel.meno
    }

    func zobrazChybu(_ error: Error) {
        let alert = UIAlertController(
            title: "Chyba",
            message: error.localizedDescription,
            preferredStyle: .alert
        )
        present(alert, animated: true)
    }
}

Koniec s tým večným „zabudol som aktualizovať UI na main threade". Filozofia je jasná — Swift by vás mal zaťažovať súbežnosťou len natoľko, nakoľko ju reálne potrebujete.

Atribút @concurrent

Keď máte zapnutú predvolenú MainActor izoláciu, potrebujete spôsob, ako z nej „vystúpiť" pre náročné operácie. Na to slúži @concurrent:

@MainActor
class SpravaObrazkov {
    var cache: [String: UIImage] = [:]

    // Táto funkcia beží na MainActor (predvolené)
    func zobrazObrazok(kluc: String) -> UIImage? {
        return cache[kluc]
    }

    // Explicitne bežiaca mimo MainActor
    @concurrent
    nonisolated func dekodujObrazok(data: Data) async -> UIImage? {
        // Beží na concurrent thread poole
        guard let obrazok = UIImage(data: data) else { return nil }

        let renderer = UIGraphicsImageRenderer(size: CGSize(width: 200, height: 200))
        let zmeneny = renderer.image { _ in
            obrazok.draw(in: CGRect(x: 0, y: 0, width: 200, height: 200))
        }

        return zmeneny
    }

    func nacitajAZobraz(url: URL) async {
        let (data, _) = try? await URLSession.shared.data(from: url)
        guard let data = data else { return }

        // Dekódovanie na pozadí
        if let obrazok = await dekodujObrazok(data: data) {
            // Späť na MainActor
            cache[url.absoluteString] = obrazok
        }
    }
}

Dôležité — @concurrent funguje iba na nonisolated funkciách. Zabezpečí, že funkcia opustí izoláciu volajúceho a beží na vlastnom vlákne. Je to presný opak predvoleného správania v Swift 6.2, kde nonisolated funkcie zdedia izoláciu od toho, kto ich zavolal.

Tri fázy zavádzania súbežnosti

Swift 6.2 podporuje postupné odhaľovanie súbežnosti, čo je naozaj premyslený prístup:

  • Fáza 1 — Jednoduchý kód: Píšete normálny sekvenčný kód. Žiadne anotácie, žiadna súbežnosť. Úplná pohoda.
  • Fáza 2 — Async/await: Pridáte async/await tam, kde volíte API, ktoré suspendujú. Kód stále vyzerá sekvenčne, len s await na správnych miestach.
  • Fáza 3 — Plný paralelizmus: Až keď vedome zavediete paralelné vykonávanie, musíte riešiť aktérov, Sendable a data race bezpečnosť.

Tento prístup je skvelý. Začiatočník môže pokojne používať async/await bez toho, aby musel rozumieť aktérom. A keď dozreje, celý ten ďalší svet je tam na neho pripravený.

Osvedčené postupy (a chyby, ktorým sa vyhnúť)

Po rokoch práce so Swift concurrency som si osvojil niekoľko pravidiel, ktoré mi ušetrili veľa bolesti hlavy. Tu sú tie najdôležitejšie.

Vždy uprednostnite štruktúrovanú súbežnosť

Preferujte async let a TaskGroup pred neštrukturovanými úlohami. Štruktúrovaná súbežnosť automaticky riadi životný cyklus úloh:

// DOBRÉ: Štruktúrovaná súbežnosť
func stahnuDataStrukturovane(urls: [URL]) async throws -> [Data] {
    try await withThrowingTaskGroup(of: Data.self) { skupina in
        for url in urls {
            skupina.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                return data
            }
        }

        var vysledky: [Data] = []
        for try await data in skupina {
            vysledky.append(data)
        }
        return vysledky
    }
}

// ZLÉ: Neštrukturované úlohy môžu "uniknúť"
func stahnuDataNestrukturovane(urls: [URL]) {
    for url in urls {
        Task.detached {
            let (data, _) = try await URLSession.shared.data(from: url)
            // Kto spracuje chybu? Kto čaká na výsledok?
        }
    }
    // Funkcia sa vráti okamžite, úlohy bežia "osirotené"
}

Nezabudnite na zrušenie úloh

Pri dlhodobých operáciách vždy kontrolujte, či nebola úloha zrušená. Inak vaša úloha zbytočne beží, aj keď jej výsledok nikto nechce:

func spracujVelkeData(data: [Int]) async throws -> Int {
    var sucet = 0

    for (index, hodnota) in data.enumerated() {
        // Kontrola zrušenia každých 1000 iterácií
        if index % 1000 == 0 {
            try Task.checkCancellation()
        }

        sucet += hodnota

        // Dá príležitosť iným úlohám
        await Task.yield()
    }

    return sucet
}

// Použitie so zrušením
let task = Task {
    let vysledok = try await spracujVelkeData(data: Array(1...1_000_000))
    print("Výsledok: \(vysledok)")
}

// Zrušenie po 2 sekundách
Task {
    try await Task.sleep(nanoseconds: 2_000_000_000)
    task.cancel()
}

Neblokujte aktéra dlhými operáciami

Toto je klasická chyba. Ak v aktérovi spustíte dlhotrvajúcu synchrónnu operáciu, zablokujete všetok ostatný prístup k jeho stavu:

// ZLÉ: Blokovanie aktéra
actor SlabyCacheManager {
    private var cache: [String: Data] = [:]

    func zpracujVelkySobor(path: String) -> Data? {
        // Synchrónne čítanie blokuje celý aktér
        let data = try? Data(contentsOf: URL(fileURLWithPath: path))
        return data
    }
}

// DOBRÉ: Náročné operácie mimo aktéra
actor LepsiCacheManager {
    private var cache: [String: Data] = [:]

    func zpracujVelkySobor(path: String) async -> Data? {
        let data = await Task.detached {
            try? Data(contentsOf: URL(fileURLWithPath: path))
        }.value

        if let data = data {
            cache[path] = data
        }
        return data
    }
}

Opatrne s @unchecked Sendable

Toto nemôžem dosť zdôrazniť. @unchecked Sendable obchádza kompilátorové kontroly — zodpovednosť za thread-safety je úplne na vás:

// NEBEZPEČNÉ: Tvrdí, že je Sendable, ale nie je thread-safe
final class NebezpecnaTrieda: @unchecked Sendable {
    var pocitadlo: Int = 0  // Data race!
}

// BEZPEČNÉ: Skutočne thread-safe implementácia
final class BezpecnaTrieda: @unchecked Sendable {
    private var _pocitadlo: Int = 0
    private let lock = NSLock()

    var pocitadlo: Int {
        lock.lock()
        defer { lock.unlock() }
        return _pocitadlo
    }

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

Oddeľujte logiku od UI

Klasický vzor ViewModel s @MainActor je váš priateľ:

@MainActor
class DataViewModel: ObservableObject {
    @Published var stav: String = ""
    @Published var data: [String] = []

    func nacitajData() async {
        stav = "Načítavam..."

        do {
            let url = URL(string: "https://api.example.com/items")!
            let (responseData, _) = try await URLSession.shared.data(from: url)
            let items = try JSONDecoder().decode([String].self, from: responseData)

            // Automaticky na MainActor
            self.data = items
            self.stav = "Načítané \(items.count) položiek"
        } catch {
            self.stav = "Chyba: \(error.localizedDescription)"
        }
    }
}

Záver

Súbežnosť v Swift 6.2 je naozaj veľký krok vpred. Kombinácia async/await, aktérov, Sendable a novej iniciatívy Approachable Concurrency robí zo Swift jeden z najbezpečnejších jazykov pre súbežné programovanie. A to nie je malá vec.

Čo si z tohto článku odniesť:

  • Async/await dáva čistý, čitateľný kód bez callback hell-u. Vyzerá to skoro ako synchronný kód a to je presne pointa.
  • Štruktúrovaná súbežnosť (async let, TaskGroup) zabezpečí, že vám žiadna úloha „neutečie" — automatické zrušenie pri chybách je bonus, ktorý oceníte.
  • Aktéri riešia zdieľaný meniteľný stav elegantne a bez manuálnej synchronizácie. Žiadne locky, žiadne semafory.
  • Sendable dáva kompilátorové záruky o bezpečnosti prenosu dát. Nechajte kompilátor, nech vám pomáha.
  • Swift 6.2 s predvolenou MainActor izoláciou a @concurrent atribútom robí celý systém ešte prístupnejším.

Keď to zhrniem — štruktúrovaná súbežnosť pred neštrukturovanými úlohami, riadne spracovanie zrušenia, neblokovať aktérov a MainActor pre UI. Dodržte tieto pravidlá a váš kód bude robustný, výkonný a (čo je nemenej dôležité) čitateľný.

A ešte jedna rada na záver: nesnažte sa obchádzať bezpečnostné mechanizmy cez @unchecked Sendable, pokiaľ to nie je naozaj nevyhnutné. Kompilátor vám chce pomôcť — nechajte ho.