Wprowadzenie — dlaczego Swift 6.2 zmienia zasady gry
Pamiętacie moment, kiedy Apple zaprezentowało Swift 6.0? Programiści dostali całkiem potężne narzędzia do pisania bezpiecznego, współbieżnego kodu — aktorów, structured concurrency i ścisłe sprawdzanie izolacji danych. Brzmi świetnie na papierze. Problem w tym, że bariera wejścia okazała się absurdalnie wysoka.
Typowy projekt SwiftUI, który wcześniej kompilował się bez zająknięcia, po włączeniu trybu strict concurrency nagle wyrzucał dziesiątki ostrzeżeń i błędów. Pojawił się nawet termin async contamination — ta frustrująca konieczność oznaczania kolejnych funkcji jako async i @Sendable, mimo że kod w gruncie rzeczy działał na jednym wątku.
No i tu wchodzi Swift 6.2.
Wydany we wrześniu 2025 roku, odpowiada na tę frustrację filozofią Approachable Concurrency — współbieżności, która jest faktycznie łatwa w adopcji. Zamiast zmuszać programistów do zrozumienia wszystkiego na raz, Swift 6.2 pozwala na stopniowe wdrażanie równoległości: zacznij od sekwencyjnego kodu, dodaj async/await tam, gdzie to potrzebne, a aktorów i @concurrent wprowadzaj dopiero wtedy, gdy faktycznie chcesz przetwarzać dane równolegle.
W tym artykule omówimy wszystkie kluczowe nowości dotyczące współbieżności w Swift 6.2 — domyślną izolację na MainActor, atrybut @concurrent, zmienione zachowanie nonisolated async, nowe narzędzia jak Task.immediate i nazwane taski, a także praktyczne wskazówki migracji. Będzie konkretnie i z kodem.
Domyślna izolacja na MainActor (SE-0466)
Najważniejsza zmiana w Swift 6.2? Możliwość ustawienia domyślnej izolacji na MainActor dla całego modułu. W praktyce oznacza to, że każda klasa, struktura, metoda i właściwość w danym module jest automatycznie izolowana na głównym aktorze — chyba że jawnie zdecydujesz inaczej.
Do tej pory typowy kod SwiftUI wymagał ręcznych oznaczeń wszędzie:
// Swift 6.0 — wymagane jawne oznaczenia
@MainActor
class DataController {
func load() { }
func save() { }
}
@MainActor
struct ContentView: View {
let controller = DataController()
var body: some View {
Button("Załaduj") {
controller.load()
}
}
}
W Swift 6.2, po włączeniu flagi kompilatora -default-isolation MainActor, ten sam kod działa bez żadnych adnotacji:
// Swift 6.2 — z domyślną izolacją MainActor
class DataController {
func load() { }
func save() { }
}
struct ContentView: View {
let controller = DataController()
var body: some View {
Button("Załaduj") {
controller.load()
}
}
}
To podejście ma ogromne znaczenie szczególnie dla projektów, które składają się głównie z kodu UI. Bądźmy szczerzy — większość logiki w typowej aplikacji SwiftUI i tak musi działać na głównym wątku, więc domyślna izolacja na MainActor po prostu eliminuje potrzebę ręcznego oznaczania setek typów i metod. Mniej boilerplate'u, ten sam efekt.
Jak włączyć domyślną izolację?
Masz dwa sposoby:
Sposób 1: Flaga kompilatora
Dodaj flagę -default-isolation MainActor w ustawieniach budowania w Xcode (Build Settings → Other Swift Flags).
Sposób 2: W Swift Package Manager
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MojaAplikacja",
targets: [
.executableTarget(
name: "MojaAplikacja",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
]
)
Kiedy nie stosować domyślnej izolacji MainActor?
To nie jest rozwiązanie na wszystko. Unikaj go w:
- Bibliotekach i frameworkach, które mają być niezależne od kontekstu wykonania
- Modułach odpowiedzialnych za ciężkie obliczenia w tle
- Kodzie sieciowym i warstwach dostępu do danych, które powinny działać poza głównym wątkiem
Funkcje nonisolated async — zmiana kontekstu wykonania (SE-0461)
Jedna z najbardziej zaskakujących zmian w Swift 6.2 dotyczy sposobu, w jaki wykonują się funkcje oznaczone jako nonisolated async. Szczerze mówiąc, stare zachowanie było po prostu nieintuicyjne:
- Funkcja
nonisolated(synchroniczna) — wykonywała się na aktorze wywołującego (np. na MainActor, jeśli była wywołana z głównego wątku). - Funkcja
nonisolated async— automatycznie przeskakiwała na globalny executor (wątek w tle), nawet jeśli nikt o to nie prosił.
To zachowanie powodowało sporo problemów. Programiści musieli oznaczać typy jako Sendable, ponieważ dane były przesyłane między aktorami, nawet gdy logicznie nie było to konieczne. Frustrujące.
Nowe domyślne zachowanie: nonisolated(nonsending)
W Swift 6.2, dzięki propozycji SE-0461, funkcje nonisolated async domyślnie zachowują się jak nonisolated(nonsending) — czyli nie zmieniają kontekstu wykonania. Wywołujesz z MainActor? Wykona się na MainActor. Z własnego aktora? Zostanie na tym aktorze. Proste i logiczne.
// Swift 6.2 — domyślne zachowanie
class NetworkService {
// Ta funkcja wykona się na aktorze wywołującego
// Nie przeskoczy na globalny executor
func fetchData() async throws -> Data {
let url = URL(string: "https://api.example.com/data")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
// Wywołanie z MainActor — fetchData() wykona się na MainActor
@MainActor
func loadContent() async {
let service = NetworkService()
do {
let data = try await service.fetchData()
// Aktualizacja UI odbywa się na tym samym wątku
updateUI(with: data)
} catch {
showError(error)
}
}
Aby włączyć to zachowanie jako upcoming feature, dodaj flagę:
.enableUpcomingFeature("NonisolatedNonsendingByDefault")
Korzyści z nowego modelu
Krótko i na temat:
- Mniej wymogów Sendable — skoro dane nie przekraczają granic aktorów, nie musisz oznaczać typów jako
Sendable. - Mniej niespodzianek — kod wykonuje się tam, skąd został wywołany. Koniec z tajemniczymi przeskokami na inne wątki.
- Prostsza migracja — istniejący kod nie zmienia nagle wątku wykonania.
Atrybut @concurrent — jawne przeniesienie na wątek w tle
Skoro domyślnie funkcje nonisolated async wykonują się na aktorze wywołującego, pojawia się naturalne pytanie: jak jawnie przenieść obliczenia na wątek w tle? Odpowiedzią jest nowy atrybut @concurrent.
class ImageProcessor {
// Ta funkcja ZAWSZE wykona się na globalnym executorze
// (w wątku w tle), niezależnie od tego, kto ją wywołuje
@concurrent
func processImage(_ image: UIImage) async -> UIImage {
// Ciężkie przetwarzanie obrazu w tle
let filtered = applyFilters(to: image)
let resized = resize(filtered, to: CGSize(width: 800, height: 600))
return resized
}
}
@MainActor
func handleImageSelection(_ image: UIImage) async {
let processor = ImageProcessor()
// processImage() przenosi się na wątek w tle
let result = await processor.processImage(image)
// Po zakończeniu wracamy na MainActor
displayImage(result)
}
Moim zdaniem, @concurrent to jedna z najlepiej zaprojektowanych zmian w tym wydaniu. Intencja programisty jest od razu jasna: ta funkcja ma działać równolegle. To realizacja zasady progressive disclosure — współbieżność pojawia się tylko wtedy, gdy jawnie o nią poprosisz.
Kiedy używać @concurrent?
- Ciężkie obliczenia — przetwarzanie obrazów, parsowanie dużych plików JSON
- Operacje I/O, które nie powinny blokować głównego wątku
- Batch processing danych
- Algorytmy kryptograficzne i kompresja
Kiedy NIE używać @concurrent?
- Proste wywołania sieciowe obsługiwane przez
URLSession(same w sobie są asynchroniczne i radzą sobie bez tego) - Funkcje, które aktualizują UI
- Kod, który operuje na izolowanych danych aktora
Task.immediate — natychmiastowe uruchomienie (SE-0472)
W Swift 6.0, kiedy tworzysz Task { ... }, closure nie jest wykonywane od razu — jest kolejkowane i uruchamiane w przyszłym cyklu pętli zdarzeń. W wielu przypadkach to zachowanie jest po prostu niepotrzebne i powoduje zauważalne opóźnienia.
Swift 6.2 wprowadza Task.immediate, który uruchamia closure natychmiast, jeśli jesteśmy już na odpowiednim executorze:
@MainActor
func setupView() {
// Task.immediate — jeśli już jesteśmy na MainActor,
// closure wykona się natychmiast, bez czekania na następny cykl
Task.immediate {
let data = await fetchInitialData()
updateUI(with: data)
}
// Tradycyjny Task — zawsze jest kolejkowany
Task {
let data = await fetchInitialData()
updateUI(with: data)
}
}
Jeśli pracowaliście ze SwiftUI, pewnie znacie to irytujące migotanie interfejsu przy inicjalizacji widoków. Task.immediate pozwala tego uniknąć — dane zaczynają się ładować od razu, bez czekania na następny cykl pętli zdarzeń. Mała zmiana, duży wpływ na UX.
Nazwane taski — lepsza diagnostyka (SE-0469)
Debugowanie współbieżnego kodu to (delikatnie mówiąc) wyzwanie. Swift 6.2 wprowadza możliwość nadawania nazw taskom, co znacznie ułatwia życie przy identyfikacji i śledzeniu w narzędziach diagnostycznych:
// Nadanie nazwy taskowi
let downloadTask = Task(name: "PobieranieProfilu") {
let profile = try await fetchUserProfile()
return profile
}
// Odczytanie nazwy bieżącego taska
Task(name: "PrzetwarzanieDanych") {
print("Aktualny task: \(Task.name ?? "Nieznany")")
// Nazwy pojawiają się w debuggerze LLDB
// oraz w Instruments
await processData()
}
Nazwane taski integrują się z narzędziami Apple, co jest naprawdę miłym dodatkiem:
- LLDB — nazwa taska jest widoczna przy breakpointach w asynchronicznym kodzie
- Instruments — łatwiejsze filtrowanie i identyfikacja tasków
- Xcode Thread Navigator — nazwy tasków pojawiają się w liście wątków
Izolowane deinicjalizatory (SE-0371)
Przed Swift 6.2 deinicjalizatory (deinit) nie mogły bezpiecznie uzyskiwać dostępu do izolowanych właściwości aktora. Klasyczny problem: masz klasę oznaczoną jako @MainActor, chcesz posprzątać zasoby w deinit, a kompilator mówi „nie".
Swift 6.2 rozwiązuje ten problem, pozwalając na oznaczenie deinicjalizatora jako isolated:
@MainActor
class MediaPlayer {
var currentTrack: Track?
var audioEngine: AudioEngine
init() {
audioEngine = AudioEngine()
}
// Izolowany deinit — bezpieczny dostęp do właściwości
// izolowanych na MainActor
isolated deinit {
audioEngine.stop()
currentTrack = nil
NotificationCenter.default.post(
name: .playerDeallocated,
object: nil
)
}
}
Słowo kluczowe isolated gwarantuje, że deinicjalizator wykona się na tym samym aktorze, na którym jest izolowany typ. Koniec z potencjalnymi wyścigami danych podczas niszczenia obiektów.
Eskalacja priorytetów tasków (SE-0462)
System operacyjny może dynamicznie zmieniać priorytety tasków — na przykład gdy użytkownik aktywnie czeka na wynik operacji, system podnosi jej priorytet. Swift 6.2 pozwala teraz na wykrywanie i reagowanie na takie zmiany, co jest całkiem sprytne:
func performHeavyComputation() async throws -> Result {
try await withTaskPriorityEscalationHandler {
// Główna logika obliczeniowa
var result = Result()
for chunk in dataChunks {
try Task.checkCancellation()
result.append(process(chunk))
}
return result
} onPriorityEscalated: { oldPriority, newPriority in
print("Priorytet zmieniony z \(oldPriority) na \(newPriority)")
// Możesz np. zwiększyć rozmiar batcha
// lub przenieść obliczenia na wydajniejszy rdzeń
}
}
To narzędzie jest szczególnie przydatne w aplikacjach wykonujących długotrwałe operacje — pozwala dynamicznie dostosowywać strategię przetwarzania do priorytetów systemowych.
Obserwacja transakcyjna — Observations (SE-0475)
Swift 6.2 wprowadza nowy typ Observations, który pozwala na strumieniowe obserwowanie zmian w obiektach @Observable. Jeśli kiedykolwiek marzyliście o prostszej alternatywie dla Combine do reaktywnego reagowania na zmiany — oto ona:
@Observable
class GameState {
var score = 0
var level = 1
var lives = 3
}
let game = GameState()
// Tworzenie strumienia obserwacji
let scoreUpdates = Observations {
game.score
}
// Reagowanie na zmiany w pętli async
Task {
for await currentScore in scoreUpdates {
print("Aktualny wynik: \(currentScore)")
if currentScore >= 1000 {
await showAchievement("Zdobyto 1000 punktów!")
}
}
}
Typ Observations działa jak AsyncSequence — emituje nowe wartości za każdym razem, gdy obserwowana właściwość się zmieni. Proste, czytelne i bez konieczności importowania Combine.
InlineArray — tablice o stałym rozmiarze (SE-0453)
Ok, to nie jest bezpośrednio powiązane z współbieżnością, ale nie mogę tego pominąć. InlineArray to jedna z najciekawszych nowości Swift 6.2 z perspektywy wydajności — tablica o stałym rozmiarze, której elementy są przechowywane inline (na stosie), bez alokacji na stercie:
// Deklaracja InlineArray
var kolory: InlineArray<4, String> = ["Czerwony", "Zielony", "Niebieski", "Żółty"]
// Typ może być inferowany
var liczby: InlineArray = [1, 2, 3, 4, 5]
// Dostęp przez indeks
kolory[0] = "Pomarańczowy"
// Iteracja
for i in kolory.indices {
print(kolory[i])
}
// Idealne do małych, stałych kolekcji
struct Vertex {
var position: InlineArray<3, Float> // x, y, z
var color: InlineArray<4, Float> // r, g, b, a
}
Benchmarki pokazują 20-30% lepszą wydajność w porównaniu do standardowego Array w przypadkach, gdy rozmiar kolekcji jest znany w czasie kompilacji. Szczególnie przydatne w:
- Grafice 3D i przetwarzaniu wektorów
- Buforach o stałym rozmiarze
- Protokołach komunikacyjnych z pakietami o ustalonej strukturze
- Embedded Swift i programowaniu niskopoziomowym
Dodatkowe usprawnienia językowe
Interpolacja stringów z wartościami domyślnymi (SE-0477)
Drobna zmiana, ale znacząco poprawia czytelność. Swift 6.2 pozwala na podanie wartości domyślnej dla opcjonalnych wartości w interpolacji stringów:
var imie: String? = nil
var wiek: Int? = 28
print("Witaj, \(imie, default: "Gościu")!") // "Witaj, Gościu!"
print("Wiek: \(wiek, default: "nieznany")") // "Wiek: 28"
Koniec z niezręcznym operatorem ?? wewnątrz interpolacji. Nareszcie.
weak let — niemutowalne słabe referencje (SE-0481)
final class Session: Sendable {
weak let user: User?
init(user: User?) {
self.user = user
}
}
// user może zostać zwolniony, ale referencja
// nie może być zmieniona na inny obiekt
weak let jest szczególnie przydatne w kontekście Sendable — niemutowalna słaba referencja jest bezpieczna do współdzielenia między wątkami, co rozwiązuje kolejny częsty ból głowy przy pracy z concurrency.
Raw identifiers — identyfikatory ze spacjami (SE-0451)
enum HTTPStatus: Int {
case `200 OK` = 200
case `404 Not Found` = 404
case `500 Internal Server Error` = 500
}
// Użycie w testach — czytelniejsze nazwy
func `test pobieranie profilu użytkownika`() async throws {
let profile = try await api.fetchProfile(id: 1)
XCTAssertNotNil(profile)
}
Przyznam, że nie jestem w 100% przekonany do tego feature'a w produkcyjnym kodzie, ale w testach? Nazwy metod testowych stają się wreszcie czytelne bez konwencji camelCase.
Praktyczny przewodnik migracji do Swift 6.2
Dobra, przechodzimy do konkretów. Migracja do nowego modelu współbieżności wymaga przemyślanego podejścia — nie wrzucaj wszystkiego na raz. Oto sprawdzony plan działania.
Krok 1: Audyt istniejącego kodu
Zanim włączysz jakiekolwiek nowe funkcje, przejrzyj swój projekt:
- Które moduły składają się głównie z kodu UI? → Kandydaci do domyślnej izolacji MainActor
- Które moduły wykonują ciężkie obliczenia w tle? → Powinny używać
@concurrent - Gdzie występują jawne adnotacje
@MainActor? → Mogą zostać usunięte po włączeniu domyślnej izolacji
Krok 2: Stopniowe włączanie feature flags
Kluczowa rada — nie włączaj wszystkich zmian naraz. Swift 6.2 pozwala na stopniowe wdrażanie:
// Package.swift — stopniowe wdrażanie
.target(
name: "MojaAplikacja",
swiftSettings: [
// Krok 1: Zacznij od tego
.enableUpcomingFeature("InferSendableFromCaptures"),
// Krok 2: Następnie dodaj
.enableUpcomingFeature("GlobalActorIsolatedTypesUsability"),
// Krok 3: Na końcu
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableUpcomingFeature("InferIsolatedConformances"),
]
)
Krok 3: Zidentyfikuj funkcje wymagające @concurrent
Po włączeniu NonisolatedNonsendingByDefault, przejrzyj swoje asynchroniczne funkcje. Te, które wcześniej automatycznie wykonywały się w tle, teraz będą działać na aktorze wywołującego. Dodaj @concurrent do tych, które naprawdę muszą działać równolegle:
// PRZED migracją — funkcja automatycznie szła na globalny executor
class DataParser {
func parseCSV(_ data: Data) async -> [Record] {
// ciężkie parsowanie...
}
}
// PO migracji — jawnie oznaczamy jako @concurrent
class DataParser {
@concurrent
func parseCSV(_ data: Data) async -> [Record] {
// ciężkie parsowanie nadal w tle
}
}
Krok 4: Testowanie
Swift 6.2 wprowadza również nowe możliwości testowania — Exit Tests (ST-0008) do testowania krytycznych awarii oraz Attachments (ST-0009) do dołączania danych diagnostycznych do raportów z testów:
import Testing
@Test func walidacjaDanychWejściowych() async throws {
let validator = InputValidator()
// Test normalnego przypadku
let result = validator.validate("poprawne dane")
#expect(result == .valid)
// Test z timeoutem
await confirmation("Operacja zakończona na czas") { confirm in
Task.immediate {
let data = try? await fetchWithTimeout()
if data != nil {
confirm()
}
}
}
}
@Test func nieprawidłoweWejściePowodujeAwarię() async throws {
await #expect(processExitsWith: .failure) {
let validator = InputValidator()
validator.validate(nil) // fatalError
}
}
Wzorce projektowe dla Swift 6.2
Nowy model współbieżności w Swift 6.2 zachęca do stosowania określonych wzorców projektowych. Oto kilka rekomendowanych podejść, które sprawdzają się w praktyce.
Wzorzec: Warstwa usług z jawną współbieżnością
Ten wzorzec dzieli odpowiedzialności: warstwa sieciowa działa w tle (dzięki @concurrent), a ViewModel i widoki pozostają na MainActor.
// Warstwa sieciowa — jawnie współbieżna
class APIClient {
@concurrent
func fetchUsers() async throws -> [User] {
let url = URL(string: "https://api.example.com/users")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([User].self, from: data)
}
@concurrent
func fetchPosts(for userId: Int) async throws -> [Post] {
let url = URL(string: "https://api.example.com/users/\(userId)/posts")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Post].self, from: data)
}
}
// ViewModel — domyślnie na MainActor
@Observable
class UsersViewModel {
var users: [User] = []
var isLoading = false
var error: Error?
private let api = APIClient()
func loadUsers() async {
isLoading = true
defer { isLoading = false }
do {
// fetchUsers() wykonuje się w tle (@concurrent)
// po zakończeniu wracamy na MainActor
users = try await api.fetchUsers()
} catch {
self.error = error
}
}
}
// Widok SwiftUI — na MainActor
struct UsersView: View {
@State private var viewModel = UsersViewModel()
var body: some View {
List(viewModel.users, id: \.id) { user in
Text(user.name)
}
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
.task {
await viewModel.loadUsers()
}
}
}
Wzorzec: Równoległe pobieranie danych
Kiedy musisz pobrać wiele zasobów jednocześnie, async let w połączeniu z @concurrent daje bardzo czysty kod:
@Observable
class DashboardViewModel {
var stats: DashboardStats?
var isLoading = false
private let api = APIClient()
func loadDashboard() async {
isLoading = true
defer { isLoading = false }
// Równoległe pobieranie wielu zasobów
async let users = api.fetchUsers()
async let posts = api.fetchPosts(for: 1)
async let notifications = api.fetchNotifications()
do {
let (u, p, n) = try await (users, posts, notifications)
stats = DashboardStats(
userCount: u.count,
postCount: p.count,
notificationCount: n.count
)
} catch {
print("Błąd ładowania dashboardu: \(error)")
}
}
}
Porównanie: Swift 6.0 vs Swift 6.2
Żeby mieć pełen obraz — oto kluczowe różnice w zachowaniu kodu współbieżnego między obiema wersjami:
- Domyślna izolacja — Swift 6.0: brak (wymaga jawnych adnotacji); Swift 6.2: możliwość ustawienia MainActor jako domyślnej
- nonisolated async — Swift 6.0: wykonuje się na globalnym executorze; Swift 6.2: wykonuje się na aktorze wywołującego
- Przeniesienie do tła — Swift 6.0: automatyczne dla nonisolated async; Swift 6.2: wymaga jawnego
@concurrent - Sendable — Swift 6.0: wymagane częściej; Swift 6.2: wymagane rzadziej dzięki zachowaniu kontekstu wykonania
- Tworzenie tasków — Swift 6.0: tylko kolejkowane; Swift 6.2:
Task.immediatedla natychmiastowego uruchomienia - Debugowanie — Swift 6.0: ograniczone; Swift 6.2: nazwane taski, lepsza integracja z LLDB
- Deinicjalizatory — Swift 6.0: nie mogą uzyskać dostępu do izolowanych danych; Swift 6.2:
isolated deinit
Wskazówki dotyczące wydajności
Na koniec kilka praktycznych porad, które warto mieć z tyłu głowy:
- Nie nadużywaj @concurrent — Każde użycie powoduje przeskoczenie na globalny executor. Jeśli operacja jest szybka, koszt przełączenia kontekstu może przewyższyć korzyści. Mierz, nie zgaduj.
- Używaj InlineArray dla małych kolekcji — Alokacja na stosie jest znacznie szybsza niż na stercie. Dla kolekcji o znanym rozmiarze (współrzędne 3D, kolory RGBA) to oczywisty wybór.
- Preferuj Task.immediate — Jeśli jesteś już na odpowiednim executorze,
Task.immediateeliminuje niepotrzebne opóźnienie kolejkowania. - Profiluj z Instruments — Nazwane taski ułatwiają identyfikację wąskich gardeł. Korzystaj z tego.
Podsumowanie
Swift 6.2 to moim zdaniem najważniejsze wydanie od czasu wprowadzenia async/await. Zamiast wymuszać pełne zrozumienie modelu aktorów od pierwszego dnia, pozwala na stopniowe wdrażanie współbieżności. Kluczowe zmiany to:
- Domyślna izolacja na MainActor — eliminuje ręczne oznaczanie setek typów
- nonisolated(nonsending) jako domyślne zachowanie — funkcje async nie przeskakują na globalny executor bez jawnego pozwolenia
- @concurrent — jasna intencja równoległego wykonania
- Task.immediate i nazwane taski — lepsza kontrola i diagnostyka
- Izolowane deinicjalizatory — bezpieczne czyszczenie zasobów
Filozofia progressive disclosure oznacza, że Swift wymaga od Ciebie tylko tyle wiedzy o współbieżności, ile faktycznie potrzebujesz. Pisz sekwencyjny kod, dodawaj async/await tam, gdzie to konieczne, a @concurrent tylko wtedy, gdy naprawdę chcesz równoległego wykonania. I szczerze? To jest dokładnie taki kierunek, w jakim język powinien zmierzać.