Proč záleží na bezpečnosti dat při souběžném zpracování
Datové závody (data races) patří mezi nejzákeřnější chyby v programování. A upřímně? Jsou to ty nejhorší. Představte si situaci: dvě vlákna současně čtou a zapisují do stejné proměnné. Výsledek? Nedeterministické chování, padání aplikace, poškozená data — a hlavně chyby, které se prakticky nedají reprodukovat při ladění. I zkušení vývojáři nad nimi stráví hodiny (a nemalé množství kávy).
Swift 6 na to přišel s docela revoluční odpovědí: kompletní kontrola souběžnosti v době kompilace. Místo toho, aby se datové závody odhalovaly až za běhu (pokud vůbec), kompilátor je teď dokáže odhalit ještě předtím, než aplikaci spustíte.
A s příchodem Swift 6.2 se celý systém ještě výrazně zjednodušil díky konceptu Approachable Concurrency.
Tak pojďme na to. V tomto průvodci si projdeme všechno, co potřebujete vědět — od základních konceptů jako Sendable a aktory, přes praktické použití Mutex z nového frameworku Synchronization, až po migrační strategii a nejnovější změny ve Swift 6.2. Připravte si Xcode, budeme psát hodně kódu.
Co jsou datové závody a proč je Swift řeší
Datový závod nastane, když dvě nebo více vláken přistupuje ke stejným datům současně a alespoň jedno z nich provádí zápis. Tady je klasický příklad:
// NEBEZPEČNÉ: Datový závod!
var counter = 0
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
counter += 1 // Více vláken čte a zapisuje současně
}
print(counter) // Výsledek: Kdo ví? 987? 993? 1000? Pokaždé jinak.
Tento kód vypadá nevinně, ale je fundamentálně rozbitý. Operace counter += 1 není atomická — skládá se z přečtení hodnoty, zvýšení o jedničku a zápisu zpět. Když dvě vlákna přečtou stejnou hodnotu současně, jedno přepsání se prostě ztratí.
Před Swift 6 jste měli k dispozici nástroje jako Thread Sanitizer (TSan), který datové závody odhaloval za běhu. Jenže problém je v tom, že TSan zachytí závod jen tehdy, když k němu skutečně dojde během testování. A datové závody jsou ze své podstaty nedeterministické — v testech se nemusí projevit vůbec, ale v produkci vám spadnou na 0,1 % uživatelů. Přesně ten typ bugu, ze kterého se vám dělá špatně.
Swift 6 tento problém řeší zásadně jinak: přesouvá detekci datových závodů do doby kompilace. Kompilátor analyzuje tok dat a izolační hranice a odmítne zkompilovat kód, který by mohl vést k datovému závodu. Je to jako mít neúnavného code reviewera, který nikdy nic nepřehlédne.
Sendable: Základ bezpečného přenosu dat
Protokol Sendable je základním kamenem celého systému bezpečnosti dat ve Swift 6. Říká kompilátoru: „Tento typ je bezpečný pro přenos mezi izolačními doménami (vlákny, aktory)."
Které typy jsou automaticky Sendable
Hodnotové typy jsou přirozeně bezpečné pro souběžný přístup, protože se při předání kopírují. Swift proto automaticky označuje jako Sendable tyto typy:
- Základní typy —
Int,String,Bool,Doublea další - Struktury — pokud všechny jejich vlastnosti jsou také
Sendable - Výčty — pokud všechny asociované hodnoty jsou
Sendable - Tuple — pokud všechny prvky jsou
Sendable - Aktory — ty jsou navrženy přímo pro bezpečný souběžný přístup
// Automaticky Sendable - všechny vlastnosti jsou Sendable
struct UserProfile {
let name: String
let age: Int
let email: String
}
// Automaticky Sendable - asociované hodnoty jsou Sendable
enum PaymentStatus {
case pending
case completed(amount: Double)
case failed(error: String)
}
Kdy Sendable nefunguje automaticky
Třídy (reference types) nejsou automaticky Sendable, protože se předávají odkazem — více vláken tak může přistupovat ke stejné instanci. Kompilátor vyžaduje explicitní konformanci a vynucuje přísná pravidla:
// Třída může být Sendable pouze pokud:
// 1. Je final
// 2. Všechny uložené vlastnosti jsou let a Sendable
final class Configuration: Sendable {
let apiUrl: String
let timeout: Int
let maxRetries: Int
init(apiUrl: String, timeout: Int, maxRetries: Int) {
self.apiUrl = apiUrl
self.timeout = timeout
self.maxRetries = maxRetries
}
}
// CHYBA: var vlastnost nemůže být v Sendable třídě
final class MutableConfig: Sendable {
var apiUrl: String // Kompilátor odmítne
init(apiUrl: String) { self.apiUrl = apiUrl }
}
@unchecked Sendable — únikový ventil
V praxi narazíte na situace, kdy víte, že váš typ je bezpečný, ale kompilátor to prostě nedokáže ověřit. Pro tyto případy existuje @unchecked Sendable:
// @unchecked Sendable - říkáme kompilátoru "věř mi"
// POZOR: Odpovědnost za bezpečnost je plně na nás!
final class ThreadSafeCache: @unchecked Sendable {
private var cache: [String: Data] = [:]
private let lock = NSLock()
func get(_ key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return cache[key]
}
func set(_ key: String, value: Data) {
lock.lock()
defer { lock.unlock() }
cache[key] = value
}
}
Tohle funguje, ale je to dost křehké. Pokud uděláte chybu v implementaci zámku, kompilátor vám vůbec nepomůže. Naštěstí Swift 6 přinesl mnohem lepší řešení — Mutex. Ale k tomu se dostaneme za chvilku.
Aktory: Izolace stavu na úrovni jazyka
Aktory jsou referenční typy (jako třídy), které automaticky serializují přístup ke svému vnitřnímu stavu. To je ta klíčová věc. Kdykoli chcete přistoupit k vlastnosti nebo metodě aktoru zvenčí, musíte použít await — kompilátor vás donutí počkat, až je přístup bezpečný.
actor BankAccount {
let accountNumber: String
private(set) var balance: Double
init(accountNumber: String, initialBalance: Double) {
self.accountNumber = accountNumber
self.balance = initialBalance
}
func deposit(_ amount: Double) {
balance += amount
}
func withdraw(_ amount: Double) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
func transfer(to other: BankAccount, amount: Double) async -> Bool {
guard balance >= amount else { return false }
balance -= amount
await other.deposit(amount) // await při přístupu k jinému aktoru
return true
}
}
Použití aktoru zvenčí vždy vyžaduje await:
let account = BankAccount(accountNumber: "CZ123", initialBalance: 10000)
// Z asynchronního kontextu
Task {
await account.deposit(5000)
let balance = await account.balance
print("Zůstatek: \(balance) Kč")
let success = await account.withdraw(3000)
if success {
print("Výběr úspěšný")
}
}
Izolace aktoru a nonisolated
Uvnitř aktoru jsou všechny metody a vlastnosti izolované — mají exkluzivní přístup ke stavu. Někdy ale chcete metodu, která nepotřebuje přístup k měnitelnému stavu. Pro to slouží klíčové slovo nonisolated:
actor UserService {
private var users: [String: User] = [:]
// Izolovaná metoda - přistupuje k users
func addUser(_ user: User) {
users[user.id] = user
}
func getUser(id: String) -> User? {
return users[id]
}
// nonisolated - nepotřebuje přístup ke stavu aktoru
// Lze volat bez await
nonisolated func formatUserName(_ name: String) -> String {
return name.trimmingCharacters(in: .whitespaces).capitalized
}
// nonisolated computed property
nonisolated var serviceName: String {
"UserService v2.0"
}
}
MainActor: Bezpečné aktualizace UI
@MainActor je globální aktor, který garantuje spuštění kódu na hlavním vlákně. Pokud děláte cokoli s UI ve SwiftUI nebo UIKit, je tohle váš nejlepší kamarád:
@MainActor
class ProfileViewModel: ObservableObject {
@Published var userName: String = ""
@Published var isLoading: Bool = false
@Published var errorMessage: String?
private let userService = UserService()
func loadProfile(userId: String) async {
isLoading = true
errorMessage = nil
do {
// Síťový požadavek běží na pozadí
let profile = try await APIClient.shared.fetchProfile(userId: userId)
// Zpátky na MainActor - aktualizace UI je bezpečná
userName = profile.name
} catch {
errorMessage = "Nepodařilo se načíst profil: \(error.localizedDescription)"
}
isLoading = false
}
}
S anotací @MainActor na celé třídě máte jistotu, že všechny vlastnosti a metody se vykonají na hlavním vlákně. Pokud voláte kód označený @MainActor z jiného kontextu, musíte použít await.
Mutex: Moderní synchronizace ze Synchronization frameworku
Tak, a tady to začíná být opravdu zajímavé. Swift 6 a iOS 18 představily zcela nový framework Synchronization, který obsahuje dvě nízkoúrovňové synchronizační primitiva: Mutex a Atomic. Pro většinu z nás je nejdůležitější právě Mutex.
Mutex je zámek navržený přímo pro Swift Concurrency. Na rozdíl od NSLock nebo os_unfair_lock je nepodmíněně Sendable — to znamená, že kompilátor rozumí jeho sémantice a dokáže ověřit bezpečnost v době kompilace. To je obrovský rozdíl.
Základní použití Mutex
import Synchronization
final class SafeCounter: Sendable {
let counter = Mutex(0) // Mutex chrání hodnotu Int
func increment() {
counter.withLock { value in
value += 1
}
}
func decrement() {
counter.withLock { value in
value -= 1
}
}
func currentValue() -> Int {
counter.withLock { value in
value
}
}
}
// Použití - plně bezpečné pro souběžný přístup
let counter = SafeCounter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask {
counter.increment()
}
}
}
print(counter.currentValue()) // Vždy 1000 ✓
Všimněte si klíčového rozdílu oproti dřívějšímu přístupu s @unchecked Sendable:
- Třída je
Sendable(ne@unchecked Sendable) — kompilátor to skutečně ověřuje - Žádný manuální
lock()/unlock()—withLockautomaticky zamkne a odemkne - Není možné zapomenout odemknout zámek — closure se o to postará za vás
- Vlastnost
counterjelet—Mutexsám je neměnný, mění se jen chráněná hodnota uvnitř
Mutex s komplexními typy
Mutex dokáže chránit libovolnou hodnotu, nejen primitivní typy. Tady je příklad se složitějším stavem aplikace:
import Synchronization
struct AppState {
var currentUser: String?
var isAuthenticated: Bool = false
var preferences: [String: String] = [:]
var notificationCount: Int = 0
}
final class StateManager: Sendable {
let state = Mutex(AppState())
func login(userName: String) {
state.withLock { state in
state.currentUser = userName
state.isAuthenticated = true
}
}
func logout() {
state.withLock { state in
state.currentUser = nil
state.isAuthenticated = false
state.preferences = [:]
}
}
func updatePreference(key: String, value: String) {
state.withLock { state in
state.preferences[key] = value
}
}
func incrementNotifications() {
state.withLock { state in
state.notificationCount += 1
}
}
func snapshot() -> AppState {
state.withLock { $0 }
}
}
Kdy použít Mutex vs. aktor
Tohle je otázka, na kterou se mě ptají dost často. Tady je jednoduché rozhodovací pravidlo:
- Mutex použijte, když potřebujete synchronní přístup (uvnitř computed properties, synchronních metod), ultra nízký overhead, nebo integraci s legacy synchronním kódem
- Aktor použijte, když pracujete s asynchronním kódem, potřebujete izolovat složitější logiku, nebo chcete naplno využít systém izolace Swift Concurrency
// Mutex: Skvělé pro synchronní přístup
final class Settings: Sendable {
private let _fontSize = Mutex(14.0)
// Computed property - synchronní, žádné await
var fontSize: Double {
get { _fontSize.withLock { $0 } }
}
func setFontSize(_ size: Double) {
_fontSize.withLock { $0 = size }
}
}
// Aktor: Skvělé pro asynchronní operace
actor NetworkCache {
private var cache: [URL: Data] = [:]
func fetch(url: URL) async throws -> Data {
if let cached = cache[url] {
return cached
}
let (data, _) = try await URLSession.shared.data(from: url)
cache[url] = data
return data
}
}
Region-Based Isolation: Chytřejší kompilátor
Jedním z největších zlepšení ve Swift 6 oproti Swift 5.10 je izolace založená na regionech (SE-0414). Pokud jste zkoušeli strict concurrency ve Swift 5.10, určitě si pamatujete tu záplavu falešných varování. Kompilátor odmítal kód, který byl ve skutečnosti naprosto bezpečný, protože nedokázal sledovat, jak hodnoty proudí programem.
SE-0414 zavádí koncept izolačních regionů. Kompilátor teď rozdělí hodnoty do regionů a sleduje, zda hodnoty z jednoho regionu mohou uniknout do jiného. Pokud dokáže prokázat, že ne-Sendable hodnota je předána bezpečně (žádné jiné vlákno k ní nemá přístup), přenos povolí:
// Ve Swift 5.10 by tohle generovalo varování
// Ve Swift 6 kompilátor ví, že je to bezpečné
func processData() async {
// Vytvoříme ne-Sendable hodnotu
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
// Předáme ji do Task - kompilátor ví, že po tomto
// bodě formatter už nepoužíváme
await Task {
let result = formatter.string(from: Date())
print(result)
}.value
}
Klíčové slovo sending
SE-0430 přidává klíčové slovo sending, které explicitně říká, že parametr bude přenesen mezi izolačními regiony:
// sending parametr - hodnota přechází do jiného regionu
func processOnBackground(sending data: sending [String]) async {
await Task.detached {
for item in data {
print("Zpracovávám: \(item)")
}
}.value
}
// Použití
let items = ["jablko", "hruška", "banán"]
await processOnBackground(sending: items)
// Po zavolání funkce už items nemůžeme použít
// v tomto izolačním regionu
Globální proměnné a Swift 6
Jednou z nejčastějších bolístek při migraci na Swift 6 jsou globální proměnné. SE-0412 vyžaduje, aby každá globální proměnná byla bezpečná pro souběžný přístup. A věřte mi, v každém větším projektu jich najdete víc, než byste čekali.
// CHYBA ve Swift 6: Globální var není bezpečná
var globalConfig = AppConfiguration()
// Řešení 1: Udělat ji konstantní
let globalConfig = AppConfiguration()
// Řešení 2: Izolovat na MainActor
@MainActor
var globalConfig = AppConfiguration()
// Řešení 3: Použít nonisolated(unsafe) - poslední možnost
nonisolated(unsafe) var globalConfig = AppConfiguration()
// Řešení 4: Mutex pro skutečně sdílený stav
let globalConfig = Mutex(AppConfiguration())
Osobně doporučuji přehodnotit, zda globální proměnnou vůbec potřebujete. Většinou je lepší ji přesunout do dependency injection kontejneru nebo do aktoru. Globální stav je skoro vždycky code smell.
Praktická migrace na Swift 6: Krok za krokem
Migrace na striktní souběžnost nemusí být bolestivá, pokud ji provedete postupně. Tady je postup, který se mi osvědčil:
Krok 1: Zapněte varování v Swift 5 režimu
Nejprve zapněte kontrolu souběžnosti jako varování, aniž byste přepínali na Swift 6 jazykový režim:
// V Package.swift
.target(
name: "MyApp",
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency")
]
)
// Nebo v Xcode:
// Build Settings → Swift Compiler - Upcoming Features
// → Strict Concurrency Checking = Complete
Tímto získáte všechna varování, ale projekt se stále zkompiluje. Můžete řešit problémy postupně, bez tlaku.
Krok 2: Začněte od listových modulů
Pokud máte projekt rozdělen do modulů, začněte od těch, které nemají žádné závislosti na jiných vašich modulech. Ty jsou nejjednodušší na migraci, protože nemusíte řešit interakci s nemigrovaným kódem.
Krok 3: Označte typy jako Sendable
Projděte své typy a rozhodněte, které potřebují být Sendable:
// Modely dat - typicky jednoduché hodnotové typy
struct Article: Sendable, Codable {
let id: UUID
let title: String
let content: String
let publishedAt: Date
}
// Konfigurační objekty - neměnné třídy
final class APIConfiguration: Sendable {
let baseURL: URL
let apiKey: String
let timeout: TimeInterval
init(baseURL: URL, apiKey: String, timeout: TimeInterval = 30) {
self.baseURL = baseURL
self.apiKey = apiKey
self.timeout = timeout
}
}
// Služby s měnitelným stavem - použijte Mutex
import Synchronization
final class AnalyticsService: Sendable {
private let events = Mutex<[AnalyticsEvent]>([])
func track(_ event: AnalyticsEvent) {
events.withLock { $0.append(event) }
}
func flush() -> [AnalyticsEvent] {
events.withLock { events in
let copy = events
events.removeAll()
return copy
}
}
}
Krok 4: Převeďte @unchecked Sendable na Mutex
Pokud máte existující kód s @unchecked Sendable, teď je ideální čas ho nahradit. Podívejte se na ten rozdíl:
// PŘED: Křehké a nekontrolované
final class ImageCache: @unchecked Sendable {
private var cache: [String: UIImage] = [:]
private let lock = NSLock()
func image(for key: String) -> UIImage? {
lock.lock()
defer { lock.unlock() }
return cache[key]
}
func store(_ image: UIImage, for key: String) {
lock.lock()
defer { lock.unlock() }
cache[key] = image
}
}
// PO: Bezpečné a ověřené kompilátorem
import Synchronization
final class ImageCache: Sendable {
private let cache = Mutex<[String: UIImage]>([:])
func image(for key: String) -> UIImage? {
cache.withLock { $0[key] }
}
func store(_ image: UIImage, for key: String) {
cache.withLock { $0[key] = image }
}
}
Mnohem čistší, že?
Krok 5: Přepněte na Swift 6 jazykový režim
Když jsou všechna varování vyřešena, přepněte na Swift 6:
// V Package.swift
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.iOS(.v18)],
targets: [
.target(name: "MyApp")
]
)
Swift 6.2: Approachable Concurrency
Pokud vám přechod na Swift 6 přišel náročný (a já vám to vůbec nemám za zlé), máme dobrou zprávu. Swift 6.2 přináší koncept Approachable Concurrency, který zásadně zjednodušuje práci se souběžností pro většinu vývojářů.
Výchozí izolace na MainActor
Tohle je asi největší změna. Nové projekty v Xcode 26 budou mít zapnutou výchozí izolaci na MainActor automaticky. Celý váš kód je ve výchozím stavu na hlavním vlákně, jako by byl jednovláknový:
// S DefaultIsolation = MainActor (Swift 6.2)
// Tento kód je automaticky @MainActor
class ProfileViewController: UIViewController {
var userName: String = "" // Bezpečné - jsme na MainActor
func updateUI() {
// Toto je na hlavním vlákně automaticky
nameLabel.text = userName
}
}
Pro zapnutí v existujícím projektu přidejte:
// V Package.swift
.target(
name: "MyApp",
swiftSettings: [
.enableExperimentalFeature("DefaultIsolation", value: "MainActor")
]
)
nonisolated(nonsending): Konzistentní chování
Swift 6.2 opravuje jednu nekonzistenci, která trápila vývojáře od začátku. Dříve se nonisolated async funkce chovaly dost neočekávaně — mohly „utéct" z aktoru volajícího a spustit se na libovolném vlákně. Nové klíčové slovo nonisolated(nonsending) zajišťuje, že funkce zůstane na vlákně volajícího:
// nonisolated(nonsending) - zůstane na vlákně volajícího
nonisolated(nonsending) func formatDate(_ date: Date) async -> String {
// Běží na stejném vlákně jako volající
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy HH:mm"
return formatter.string(from: date)
}
// S příznakem NonIsolatedNonSendingByDefault je toto výchozí chování
// pro všechny nonisolated funkce
@concurrent: Explicitní offloading
Když chcete explicitně spustit práci na pozadí, použijte nový atribut @concurrent:
// @concurrent - explicitně běží mimo aktor volajícího
@concurrent
func processImage(_ data: Data) async -> UIImage? {
// Toto běží na background vlákně
// Vhodné pro výpočetně náročné operace
guard let image = UIImage(data: data) else { return nil }
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 200, height: 200))
return renderer.image { context in
image.draw(in: CGRect(origin: .zero, size: CGSize(width: 200, height: 200)))
}
}
// Volání z MainActor kontextu
@MainActor
func handleImageUpload(_ data: Data) async {
// processImage se automaticky přesune na pozadí
if let thumbnail = await processImage(data) {
imageView.image = thumbnail // Zpět na MainActor
}
}
Tento model je mnohem intuitivnější: vše je ve výchozím stavu na hlavním vlákně, a pouze to, co explicitně označíte @concurrent, se přesune na pozadí. Žádné nechtěné přepínání vláken, žádná překvapení. Konečně to dává smysl.
Časté chyby a jak je řešit
Při práci se Swift 6 souběžností narazíte na několik typických problémů. Pojďme si projít ty nejčastější — a hlavně jak se jich zbavit.
Chyba: Closure předaná do funkce se Sendable parametrem
// CHYBA: Capture of 'self' with non-sendable type
class DataProcessor {
var results: [String] = []
func process() {
Task {
// self není Sendable - kompilátor hlásí chybu
results.append("zpracováno")
}
}
}
// ŘEŠENÍ: Použijte aktor
actor DataProcessor {
var results: [String] = []
func process() {
Task {
// Uvnitř aktoru je to bezpečné
await self.appendResult("zpracováno")
}
}
func appendResult(_ result: String) {
results.append(result)
}
}
Chyba: Předání delegáta mezi izolačními doménami
// PROBLÉM: Delegate pattern s ne-Sendable protokolem
protocol DataDelegate: AnyObject {
func didReceiveData(_ data: Data)
}
// ŘEŠENÍ: Označte protokol jako MainActor
@MainActor
protocol DataDelegate: AnyObject {
func didReceiveData(_ data: Data)
}
// Nebo použijte Sendable closure místo delegáta
actor DataLoader {
func load(url: URL, completion: @Sendable (Data) -> Void) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
completion(data)
}
}
Chyba: Práce s ne-Sendable typy z frameworků
// PROBLÉM: DateFormatter není Sendable
actor FormattingService {
func format(date: Date) -> String {
let formatter = DateFormatter() // Vytvořte lokálně
formatter.dateFormat = "dd.MM.yyyy"
return formatter.string(from: date)
}
}
// ALTERNATIVA: Použijte Sendable alternativy
// Date.FormatStyle je Sendable
func formatDate(_ date: Date) -> String {
date.formatted(.dateTime.day().month().year())
}
Testování souběžného kódu
Testování souběžného kódu vyžaduje trochu jiný přístup než běžné unit testy. Swift Testing framework naštěstí nabízí elegantní podporu pro async testy:
import Testing
import Synchronization
@Test
func testSafeCounterConcurrency() async {
let counter = SafeCounter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask { counter.increment() }
}
for _ in 0..<500 {
group.addTask { counter.decrement() }
}
}
#expect(counter.currentValue() == 500)
}
@Test
func testBankAccountTransfer() async {
let alice = BankAccount(accountNumber: "A1", initialBalance: 1000)
let bob = BankAccount(accountNumber: "B1", initialBalance: 500)
let success = await alice.transfer(to: bob, amount: 300)
#expect(success)
#expect(await alice.balance == 700)
#expect(await bob.balance == 800)
}
@Test
@MainActor
func testViewModelLoading() async {
let viewModel = ProfileViewModel()
await viewModel.loadProfile(userId: "test-user")
#expect(!viewModel.isLoading)
#expect(viewModel.errorMessage == nil)
}
Doporučené vzory a praktiky
Na závěr si shrňme doporučené vzory pro bezpečnou souběžnost ve Swift 6. Jsou to pravidla, která se mi v praxi opravdu osvědčila.
1. Preferujte hodnotové typy
Struktury a výčty jsou automaticky Sendable. Kdykoli je to možné, modelujte svá data jako hodnotové typy:
// Preferujte
struct NetworkRequest: Sendable {
let url: URL
let method: HTTPMethod
let headers: [String: String]
}
// Místo
class NetworkRequest { ... }
2. Používejte Mutex pro synchronní přístup
Kdykoliv potřebujete synchronní přístup ke sdílenému stavu, sáhněte po Mutex. Vyhnete se @unchecked Sendable a získáte plné ověření od kompilátoru.
3. Aktory pro asynchronní izolaci
Pro služby a manažery, které pracují s asynchronním kódem, jsou aktory tou nejpřirozenější volbou. Využijte jejich izolaci stavu a automatickou serializaci přístupu.
4. Minimalizujte kritické sekce
// ŠPATNĚ: Příliš dlouhá kritická sekce
let data = Mutex<[Item]>([])
data.withLock { items in
let processed = items.map { expensiveOperation($0) } // Blokuje ostatní vlákna!
items = processed
}
// SPRÁVNĚ: Minimální kritická sekce
let snapshot = data.withLock { Array($0) } // Rychlé zkopírování
let processed = snapshot.map { expensiveOperation($0) } // Mimo zámek
data.withLock { $0 = processed } // Rychlý zápis
5. Postupná migrace
Nesnažte se migrovat celý projekt najednou. Zapněte varování, opravte je modul po modulu, a teprve když je vše čisté, přepněte na Swift 6. S příchodem Swift 6.2 a Approachable Concurrency je migrace jednodušší než kdy dřív.
Závěr
Swift 6 přinesl zásadní posun v tom, jak přistupujeme k souběžnému programování. Datové závody, které dříve představovaly noční můru každého vývojáře, teď zachytí kompilátor ještě před spuštěním aplikace. Máme k dispozici mocné nástroje:
- Sendable pro bezpečný přenos dat mezi izolačními doménami
- Aktory pro automatickou izolaci a serializaci stavu
- Mutex pro efektivní synchronní přístup ke sdíleným datům
- Region-based isolation pro chytřejší analýzu toku dat
- Approachable Concurrency ve Swift 6.2 pro jednodušší mentální model
Klíčové poselství je jasné: bezpečnost souběžnosti ve Swift není jen o dodržování pravidel kompilátoru. Je to o psaní kódu, kterému můžete důvěřovat. Kódu, který nehavaruje v produkci kvůli zákeřnému datovému závodu, který se projevil jen u zlomku uživatelů.
A s každou novou verzí Swift se tento cíl stává dosažitelnějším.
Začněte ještě dnes — zapněte si StrictConcurrency varování ve svém projektu a podívejte se, co kompilátor najde. Možná budete překvapeni. Já rozhodně byl.