Bevezetés: Miért változtat meg mindent a Swift 6.2?
Valljuk be őszintén – a Swift párhuzamossági modellje az elmúlt években nem kis fejtörést okozott a fejlesztőknek. A Swift 5.5 async/await rendszere igazi áttörés volt, de aztán jött a Swift 6.0 strict concurrency ellenőrzése, és sokan éreztük úgy, hogy a Sendable figyelmeztetések és az actor izolációs hibák özöne egyszerűen túl sok. Ami korábban három sor volt, az hirtelen tizenöt lett. Ismerős?
Nos, a Swift 6.2 „Approachable Concurrency" funkciócsomagja pont erre a fájdalomra ad írt.
Ez az útmutató végigvezet a Swift 6.2 párhuzamossági újdonságain, gyakorlati kódpéldákkal és migrációs tanácsokkal. Akár egy meglévő projektet frissítesz, akár most kezdesz egy újat – itt mindent megtalálsz, amire szükséged lesz. Szóval, vágjunk bele!
Mi az az Approachable Concurrency?
Az Approachable Concurrency nem egyetlen funkció, hanem több, egymást kiegészítő Swift Evolution javaslat (proposal) gyűjteménye. Ezek együttesen egyszerűsítik a párhuzamos kód írását. A fő cél: lehetővé tenni, hogy fokozatosan vezesd be a párhuzamosságot a kódodba, ahelyett hogy azonnal meg kellene küzdened az összes strict concurrency követelménnyel.
A legfontosabb változások:
- nonisolated(nonsending) alapértelmezés – Az aszinkron függvények a hívó actor-ján futnak alapértelmezetten
- @concurrent attribútum – Explicit jelzés, amikor egy függvényt külön szálon szeretnénk futtatni
- Alapértelmezett MainActor izoláció – Modulszintű MainActor izoláció UI-alkalmazásokhoz
- Javított Sendable következtetés – A fordító okosabban dönti el, mi Sendable és mi nem
- Izolált protokoll-megfelelőségek – Actor-izolált típusok könnyebb protokoll-implementációja
A nonisolated(nonsending) alapértelmezés (SE-0461)
Ez talán a legjelentősebb változás a Swift 6.2-ben. A korábbi verziókban egy nonisolated async függvény mindig a globális végrehajtón (global executor) futott – vagyis egy tetszőleges háttérszálon. Ez rengeteg felesleges szálváltást és Sendable-ellenőrzést eredményezett, ami őszintén szólva frusztráló volt.
A régi viselkedés (Swift 6.1 és korábbi)
class DataManager {
nonisolated func fetchData() async -> [String] {
// Ez mindig egy háttérszálon futott,
// még ha a MainActor-ról hívtuk is!
// Szálváltás történt minden alkalommal.
return await loadFromNetwork()
}
}
@MainActor
class ViewModel: ObservableObject {
@Published var items: [String] = []
func refresh() async {
let manager = DataManager()
// A fetchData() itt szálváltást okozott a háttérszálra,
// majd a visszatéréskor újra a MainActor-ra kellett váltani
items = await manager.fetchData()
}
}
Az új viselkedés (Swift 6.2)
class DataManager {
// Swift 6.2-ben ez alapértelmezetten a hívó actor-ján fut!
// Nincs felesleges szálváltás.
func fetchData() async -> [String] {
return await loadFromNetwork()
}
}
@MainActor
class ViewModel: ObservableObject {
@Published var items: [String] = []
func refresh() async {
let manager = DataManager()
// A fetchData() most a MainActor-on fut,
// mert onnan hívtuk. Nincs szálváltás!
items = await manager.fetchData()
}
}
Ez a változás drámaian csökkenti a felesleges szálváltásokat és az ezzel járó Sendable-követelményeket. Ha egy függvény a hívó actor-ján fut, nincs szükség arra, hogy a paraméterek és a visszatérési érték Sendable legyen – hiszen nem lépnek át párhuzamossági határokon. Ez szerintem az egyik legjobb döntés, amit a Swift csapat hozott az utóbbi időben.
Mikor fontos ez?
Gondolj bele: egy tipikus iOS alkalmazásban a legtöbb kód a MainActor-on fut. Ha egy segédfüggvény aszinkron, de valójában nem végez CPU-intenzív munkát (pl. csak egy hálózati kérést indít, ami I/O-ra vár), nincs értelme háttérszálra küldeni. Az új alapértelmezéssel a fordító ezt automatikusan kezeli.
A @concurrent attribútum
Oké, de ha az aszinkron függvények mostantól a hívó actor-ján futnak, hogyan tudjuk jelezni, hogy egy függvényt szándékosan háttérszálon szeretnénk futtatni? Erre szolgál a @concurrent attribútum.
class ImageProcessor {
// Ez a függvény szándékosan a globális végrehajtón fut,
// mert CPU-intenzív munkát végez
@concurrent
func processImage(_ data: Data) async -> UIImage? {
// Nehéz képfeldolgozási műveletek...
let processed = applyFilters(to: data)
let resized = resize(processed, to: targetSize)
return UIImage(data: resized)
}
// Ez a függvény a hívó actor-ján fut (alapértelmezés)
func fetchImageURL() async -> URL? {
return await networkClient.getLatestImageURL()
}
}
@MainActor
class PhotoViewModel: ObservableObject {
@Published var image: UIImage?
let processor = ImageProcessor()
func loadPhoto() async {
guard let url = await processor.fetchImageURL() else { return }
let data = try? await URLSession.shared.data(from: url).0
// A processImage @concurrent, tehát háttérszálon fut
// Ez helyes, mert CPU-intenzív munkát végez
if let data {
image = await processor.processImage(data)
}
}
}
A @concurrent attribútum használata gondos mérlegelést igényel. Általános szabályként: csak akkor használd, ha a függvény valóban CPU-intenzív munkát végez, és profitál a párhuzamos végrehajtásból. A legtöbb I/O-alapú művelet (hálózat, fájlrendszer, adatbázis) jobban jár a hívó actor-ján maradva.
Alapértelmezett MainActor izoláció (SE-0466)
A Swift 6.2 bevezet egy fordítói beállítást, amellyel egy teljes modult alapértelmezetten a MainActor-hoz rendelhetünk. Ez különösen hasznos UI-alkalmazásoknál, ahol a kód túlnyomó része amúgy is a fő szálon fut. (Legyünk őszinték, az esetek 90%-ában ez a helyzet.)
Engedélyezés Xcode-ban
Az Xcode Build Settings menüjében keresd a Default Actor Isolation beállítást, és állítsd MainActor-ra. Vagy használd a fordító flageket:
// Xcode Build Settings:
// Swift Compiler - Upcoming Features
// Default Actor Isolation: MainActor
// Vagy parancssorból:
// swiftc -default-isolation MainActor
Hogyan működik a gyakorlatban?
// -default-isolation MainActor engedélyezve
// Ez a struct implicit módon @MainActor
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Szám: \(count)")
Button("Növelés") {
count += 1
}
}
}
}
// Ez az osztály is implicit @MainActor
class UserSettings: ObservableObject {
@Published var username = ""
@Published var darkMode = false
func save() {
// Biztonságosan hozzáférhetünk a @Published tulajdonságokhoz
UserDefaults.standard.set(username, forKey: "username")
UserDefaults.standard.set(darkMode, forKey: "darkMode")
}
}
// Ha egy típust kifejezetten NEM akarunk a MainActor-on,
// használjuk a nonisolated kulcsszót:
nonisolated class BackgroundWorker {
func heavyComputation() async -> Int {
// Ez nem a MainActor-on fut
return expensiveCalculation()
}
}
Ez a megközelítés visszahozza azt az egyszerűséget, amit a UIKit-korszakban megszoktunk – ahol szinte minden a fő szálon futott – de most a fordító típusbiztonságával párosítva. Nekem személyesen ez volt a legkellemesebb meglepetés, amikor először kipróbáltam.
Javított Sendable következtetés (SE-0418)
A Swift 6.2 fordítója végre okosabb lett a Sendable megfelelőség automatikus következtetésében. Most már képes felismerni, hogy egy closure Sendable, ha a kontextusból ez egyértelmű.
actor BankAccount {
private var balance: Double = 0
func transfer(amount: Double, to other: BankAccount) async {
guard balance >= amount else { return }
balance -= amount
// Swift 6.1-ben ez Sendable hibát adott volna,
// mert az 'amount' Double-t closure-ben használjuk.
// Swift 6.2-ben a fordító felismeri, hogy a Double
// értéktípus, tehát biztonságosan másolható.
await other.deposit(amount: amount)
}
func deposit(amount: Double) {
balance += amount
}
}
// A fordító automatikusan következteti a Sendable-t
struct TransactionRecord {
let id: UUID
let amount: Double
let timestamp: Date
// Minden mező Sendable értéktípus,
// tehát a struct automatikusan Sendable
}
Task.immediate – Azonnali feladat-végrehajtás (SE-0472)
Egy másik nagyon hasznos újdonság a Task.immediate. Ezzel egy Task azonnal elkezdhet futni a hívó végrehajtóján, az első await pontig szinkron módon. Ezt a funkciót már nagyon vártam.
@MainActor
class SearchViewModel: ObservableObject {
@Published var results: [SearchResult] = []
@Published var isSearching = false
func search(query: String) {
// A Task.immediate azonnal elkezd futni a MainActor-on
Task.immediate {
// Ez a rész szinkron, azonnal végrehajtódik
isSearching = true
results = [] // Előző eredmények törlése
// Itt történik az első await,
// ettől a ponttól aszinkron
let newResults = await performSearch(query: query)
results = newResults
isSearching = false
}
// A Task.immediate garantálja, hogy az isSearching = true
// már beállítódott, mielőre ide érünk
}
}
A hagyományos Task { } esetén a closure végrehajtása későbbre ütemeződik. A Task.immediate viszont garantálja, hogy a szinkron kód az első await-ig azonnal lefut. Ez különösen hasznos, amikor UI-állapotot kell azonnal frissíteni egy aszinkron művelet indítása előtt.
Izolált deinitializáló – isolated deinit (SE-0371)
A Swift 6.2 megoldotta az actor-izolált osztályok egyik régóta fennálló (és elég bosszantó) problémáját: a deinitializáló most már hozzáférhet az actor-izolált állapothoz.
@MainActor
class DocumentEditor {
private var unsavedChanges: [Change] = []
private var autoSaveTimer: Timer?
init() {
autoSaveTimer = Timer.scheduledTimer(
withTimeInterval: 30,
repeats: true
) { [weak self] _ in
self?.autoSave()
}
}
// Swift 6.2 előtt a deinit nem férhetett hozzá
// az actor-izolált tulajdonságokhoz!
isolated deinit {
// Most már biztonságosan hozzáférhetünk
autoSaveTimer?.invalidate()
if !unsavedChanges.isEmpty {
saveChangesToDisk(unsavedChanges)
}
}
private func autoSave() {
guard !unsavedChanges.isEmpty else { return }
saveChangesToDisk(unsavedChanges)
unsavedChanges.removeAll()
}
private func saveChangesToDisk(_ changes: [Change]) {
// Mentés implementációja...
}
}
Task elnevezése – Könnyebb hibakeresés (SE-0469)
Kis funkció, nagy hatás: a Swift 6.2-ben a Task-ok nevet kaphatnak. Ha valaha próbáltál már hibát keresni egy tucatnyi párhuzamos Task között az Instruments-ben, tudod, mennyire hiányzott ez.
func loadDashboard() async {
await withTaskGroup(of: Void.self) { group in
group.addTask(name: "Felhasználói profil betöltése") {
await self.loadUserProfile()
}
group.addTask(name: "Értesítések lekérdezése") {
await self.fetchNotifications()
}
group.addTask(name: "Statisztikák frissítése") {
await self.updateStatistics()
}
}
}
// Önálló Task névvel
func startBackgroundSync() {
Task(name: "Háttérszinkronizáció") {
while !Task.isCancelled {
await syncWithServer()
try? await Task.sleep(for: .seconds(300))
}
}
}
A Task nevek megjelennek az Xcode debuggerben, az Instruments-ben és a naplóbejegyzésekben. Hidd el, ez a „kis" fejlesztés rengeteg időt spórol hibakeresésnél.
InlineArray – Fix méretű tömbök (SE-0453)
Bár nem közvetlenül párhuzamossági funkció, az InlineArray fontos teljesítményjavulást hozhat, különösen párhuzamos kódban, ahol a heap-allokációk könnyen szűk keresztmetszetté válhatnak.
// InlineArray: fix méretű, stack-en tárolt tömb
var planets: InlineArray<4, String> = ["Merkúr", "Vénusz", "Föld", "Mars"]
// Indexeléssel elérhető, mint egy normál tömb
print(planets[0]) // "Merkúr"
print(planets[2]) // "Föld"
// De NEM lehet hozzáadni vagy eltávolítani elemeket!
// planets.append("Jupiter") // Fordítási hiba!
// Kiváló teljesítmény-kritikus helyzetekben:
struct Particle {
var position: InlineArray<3, Float> // x, y, z
var velocity: InlineArray<3, Float>
var color: InlineArray<4, Float> // r, g, b, a
}
// 20-30%-os teljesítményjavulás a heap-allokáció elkerülésével
func simulateParticles(_ particles: inout [Particle]) {
for i in particles.indices {
for axis in 0..<3 {
particles[i].position[axis] += particles[i].velocity[axis]
}
}
}
Gyakorlati migrációs útmutató
Most, hogy áttekintettük az új funkciókat, nézzük meg, hogyan migráljunk egy meglévő projektet Swift 6.2-re. Tapasztalatból mondom: a fokozatos megközelítés a nyerő stratégia.
1. lépés: Funkciók fokozatos engedélyezése
Fontos: Ne engedélyezd az összes Approachable Concurrency funkciót egyszerre! Egyenként add hozzá őket, és minden lépésnél győződj meg róla, hogy semmi nem tört el.
// Package.swift
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.iOS(.v17)],
targets: [
.target(
name: "MyApp",
swiftSettings: [
// Kezdd ezzel – ez a legkevésbé invazív
.enableUpcomingFeature("InferSendableFromCaptures"),
// Majd add hozzá ezeket egyenként:
// .enableUpcomingFeature("GlobalActorIsolatedTypesUsability"),
// .enableUpcomingFeature("InferIsolatedConformances"),
// .enableUpcomingFeature("DisableOutwardActorInference"),
// Ezt hagyd utoljára – ez változtatja meg
// legjobban a kód viselkedését:
// .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
]
)
]
)
2. lépés: A NonisolatedNonsendingByDefault migrálása
Ez a legnagyobb viselkedésváltozás. Az aszinkron függvények ezentúl a hívó actor-ján futnak, nem a globális végrehajtón. Azonosítsd azokat a függvényeket, amelyek szándékosan háttérszálon kell fussanak:
// ELŐTTE (Swift 6.1):
class DataProcessor {
nonisolated func processLargeDataset(_ data: [Record]) async -> [ProcessedRecord] {
// Ez háttérszálon futott – ez volt az elvárt viselkedés,
// mert CPU-intenzív
return data.map { transform($0) }
}
}
// UTÁNA (Swift 6.2):
class DataProcessor {
@concurrent // Explicit jelzés: háttérszálon fusson!
func processLargeDataset(_ data: [Record]) async -> [ProcessedRecord] {
return data.map { transform($0) }
}
}
3. lépés: Sendable figyelmeztetések feloldása
A javított Sendable következtetésnek köszönhetően sok korábbi figyelmeztetés automatikusan eltűnik. De ha mégis marad néhány:
// Ha egy típus nem Sendable, de tudjuk, hogy biztonságos:
// (Csak akkor használd, ha valóban biztonságos!)
struct LegacyWrapper: @unchecked Sendable {
let connection: OldDatabaseConnection
// A connection thread-safe, de a típusa nem jelöli ezt
}
// Jobb megoldás: ha lehetséges, használj actor-t
actor SafeDatabaseConnection {
private let connection: OldDatabaseConnection
func query(_ sql: String) async -> [Row] {
return connection.execute(sql)
}
}
4. lépés: Izolált protokoll-megfelelőségek (SE-0470)
// Swift 6.1-ben ez hibát adott:
@MainActor
class AppSettings: Equatable {
var theme: String
var fontSize: Int
// Hiba: Equatable nem lehet @MainActor izolált
static func == (lhs: AppSettings, rhs: AppSettings) -> Bool {
lhs.theme == rhs.theme && lhs.fontSize == rhs.fontSize
}
}
// Swift 6.2-ben explicit izolált megfelelőség:
@MainActor
class AppSettings: @MainActor Equatable {
var theme: String
var fontSize: Int
static func == (lhs: AppSettings, rhs: AppSettings) -> Bool {
lhs.theme == rhs.theme && lhs.fontSize == rhs.fontSize
}
}
Teljes gyakorlati példa: Modern SwiftUI alkalmazás Swift 6.2-vel
Nézzünk egy komplett példát, ami az összes tárgyalt funkciót integrálja. Ezt a mintát a saját projektjeimben is hasonlóan alkalmazom.
// Package.swift vagy Xcode beállítás:
// -default-isolation MainActor
// Összes Approachable Concurrency funkció engedélyezve
import SwiftUI
// Implicit @MainActor az alapértelmezett izoláció miatt
struct Article: Identifiable, Codable {
let id: UUID
var title: String
var content: String
var tags: InlineArray<4, String>
let createdAt: Date
}
// Actor a hálózati réteghez – ez NEM a MainActor-on fut
actor ArticleService {
private let baseURL = URL(string: "https://api.example.com")!
private var cache: [UUID: Article] = [:]
func fetchArticles() async throws -> [Article] {
let url = baseURL.appending(path: "articles")
let (data, _) = try await URLSession.shared.data(from: url)
let articles = try JSONDecoder().decode([Article].self, from: data)
// Cache frissítése az actor-on belül – thread-safe
for article in articles {
cache[article.id] = article
}
return articles
}
func getCached(id: UUID) -> Article? {
return cache[id]
}
}
// ViewModel – implicit @MainActor
@Observable
class ArticleListViewModel {
var articles: [Article] = []
var isLoading = false
var errorMessage: String?
private let service = ArticleService()
func loadArticles() {
// Task.immediate: az isLoading azonnal beállítódik
Task.immediate {
isLoading = true
errorMessage = nil
do {
articles = try await service.fetchArticles()
} catch {
errorMessage = "Hiba történt: \(error.localizedDescription)"
}
isLoading = false
}
}
@concurrent
func searchArticles(query: String) async -> [Article] {
// CPU-intenzív szűrés háttérszálon
return articles.filter { article in
article.title.localizedCaseInsensitiveContains(query) ||
article.content.localizedCaseInsensitiveContains(query)
}
}
}
// SwiftUI nézet – implicit @MainActor
struct ArticleListView: View {
@State private var viewModel = ArticleListViewModel()
@State private var searchText = ""
@State private var filteredArticles: [Article] = []
var body: some View {
NavigationStack {
Group {
if viewModel.isLoading {
ProgressView("Betöltés...")
} else if let error = viewModel.errorMessage {
ContentUnavailableView(
"Hiba",
systemImage: "exclamationmark.triangle",
description: Text(error)
)
} else {
articleList
}
}
.navigationTitle("Cikkek")
.searchable(text: $searchText)
.onChange(of: searchText) { _, newValue in
Task(name: "Cikkek keresése") {
if newValue.isEmpty {
filteredArticles = viewModel.articles
} else {
filteredArticles = await viewModel.searchArticles(
query: newValue
)
}
}
}
.task {
viewModel.loadArticles()
}
}
}
private var articleList: some View {
List(filteredArticles.isEmpty ? viewModel.articles : filteredArticles) { article in
NavigationLink(value: article.id) {
VStack(alignment: .leading, spacing: 4) {
Text(article.title)
.font(.headline)
Text(article.content.prefix(100) + "...")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
}
Prioritás-eszkaláció kezelése (SE-0462)
A Swift 6.2 új API-t biztosít a feladat-prioritás változásainak kezelésére is. Ez kritikus lehet reszponzív alkalmazásoknál:
actor DownloadManager {
func downloadFile(from url: URL) async throws -> Data {
var currentPriority = Task.currentPriority
return try await withTaskPriorityEscalationHandler {
// A tényleges letöltés
let (data, _) = try await URLSession.shared.data(from: url)
return data
} onPriorityEscalated: { newPriority in
// Ha a felhasználó interakcióba lép az alkalmazással,
// a rendszer megemelheti a prioritást
print("Prioritás emelve: \(currentPriority) → \(newPriority)")
// Itt frissíthetjük a URLSession konfigurációt,
// vagy értesíthetjük a felhasználót
}
}
}
Gyakori buktatók és megoldásaik
Az Approachable Concurrency bevezetése nem mentes a kihívásoktól. Nézzük a leggyakoribb problémákat, amikbe bele lehet futni.
1. buktató: A viselkedésváltozás nem látszik fordítási időben
Ez az, amire a leginkább oda kell figyelni. A NonisolatedNonsendingByDefault engedélyezése megváltoztatja a futásidejű viselkedést, de fordítási hibát nem feltétlenül kapsz. Egy függvény, ami korábban háttérszálon futott, most a MainActor-on fog futni – és ha az lassú szinkron munkát végez, blokkolhatja a UI-t:
// VESZÉLY: Ez a kód a MainActor-on fog futni Swift 6.2-ben,
// ami blokkolhatja a UI-t!
func processHugeFile(_ path: String) async -> [Record] {
let data = loadFile(path) // Szinkron, lassú művelet
return parseRecords(data) // Szinkron, lassú művelet
}
// MEGOLDÁS: Jelöld @concurrent-ként
@concurrent
func processHugeFile(_ path: String) async -> [Record] {
let data = loadFile(path)
return parseRecords(data)
}
2. buktató: Actor re-entrancy
Ez nem új probléma, de érdemes emlékezni rá:
actor Counter {
var value = 0
func incrementTwice() async {
value += 1
// FIGYELEM: Az await pont után a value
// megváltozhatott más task által!
await someAsyncOperation()
value += 1
// A végeredmény NEM feltétlenül az eredeti + 2
}
// MEGOLDÁS: Szinkron művelet, nincs await pont
func safeIncrementTwice() {
value += 1
value += 1
// Ez atomikus az actor-on belül
}
}
3. buktató: @concurrent és Sendable
Ha @concurrent-et használsz, a paramétereknek Sendable-nek kell lenniük – hiszen ilyenkor párhuzamossági határt lépnek át:
class MutableConfig {
var timeout: Int = 30
// Ez NEM Sendable – referencia típus változtatható állapottal
}
// Ez fordítási hibát ad:
// @concurrent
// func configure(with config: MutableConfig) async { ... }
// MEGOLDÁS 1: Struct használata
struct Config: Sendable {
let timeout: Int
}
// MEGOLDÁS 2: Actor használata
actor ConfigStore {
var timeout: Int = 30
func getTimeout() -> Int { timeout }
}
Teljesítménytippek
A Swift 6.2 párhuzamossági újdonságai nem csak az ergonómiát javítják, hanem valódi teljesítménynövekedést is hozhatnak, ha jól használod őket:
- Kerüld a felesleges @concurrent jelölést – A szálváltás költséges. Ha egy függvény nem CPU-intenzív, hagyd a hívó actor-ján.
- Használj InlineArray-t teljesítmény-kritikus kódban – A stack-allokáció nagyságrendekkel gyorsabb a heap-allokációnál.
- Task.immediate a UI-reszponzivitásért – Amikor az állapotot azonnal frissíteni kell az aszinkron művelet előtt.
- Profilozz az Instruments-szel – Az Xcode Instruments Swift Concurrency eszköze most már a Task neveket is mutatja, ami sokat segít a szűk keresztmetszetek azonosításában.
- Strukturált párhuzamosság a strukturálatlan helyett – A
TaskGrouphatékonyabb, mint az önállóTask { }, és automatikusan kezeli a törlést is.
Összefoglalás
A Swift 6.2 Approachable Concurrency funkciócsomagja jelentős mérföldkő a Swift párhuzamossági modelljének fejlődésében. Röviden a lényeg:
- A nonisolated(nonsending) alapértelmezés drámaian csökkenti a felesleges szálváltásokat és Sendable-követelményeket
- A @concurrent attribútum világos szándékot fejez ki a párhuzamos végrehajtáshoz
- Az alapértelmezett MainActor izoláció visszahozza a UIKit-korszak egyszerűségét, de típusbiztonság mellett
- A Task.immediate és a Task nevek javítják a napi fejlesztői élményt
- Az InlineArray és az isolated deinit fontos teljesítmény- és biztonsági javítások
A migrációt érdemes fokozatosan végezni: engedélyezd a funkciókat egyenként, és minden lépésnél ellenőrizd, hogy az alkalmazásod helyesen működik. Amit javaslok: kezdj el kísérletezni az Approachable Concurrency funkcióival egy kisebb projektben, és fokozatosan terjeszd ki a meglévő kódbázisodra. Egy-két nap próbálkozás után meglátod, mennyivel természetesebb lesz a párhuzamos kód írása.