
Swift 6 Async/Await & Structured Concurrency: Hướng Dẫn Từ Cơ Bản Đến Production-Ready
Swift 6 (ra mắt WWDC 2024) và Swift 6.2 (WWDC 2025) đưa Structured Concurrency trở thành default – async/await, Actors, Sendable, Task Groups, AsyncSequence trở thành bắt buộc cho code an toàn, không data race. Bài viết đi từ cơ bản đến pattern production, migration strategy, và best practices cho iOS/macOS developer.
Tại Sao Swift Concurrency Quan Trọng?
Trước Swift 5.5 (2021), async code dùng completion closures – dẫn đến “callback hell”, retain cycles, khó reasoning về execution order, data race tiềm ẩn. Swift Concurrency giải quyết:
- Linear execution flow: Code async đọc như sync – dễ debug, dễ maintain
- Compile-time data race safety: Swift 6 strict concurrency checking bắt lỗi tại compile time
- Structured lifecycle: Task tự động cancel khi parent cancel – không leak resources
- Actor isolation: Bảo vệ shared state không cần lock/manual sync
Async/Await Cơ Bản – Từ Closure Đến Structured
1. Async Method Definition
// Completion closure (legacy)
func fetchImages(completion: @escaping (Result) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { completion(.failure(error)); return }
let images = parse(data)
completion(.success(images))
}.resume()
}
// Async/await (modern) - Swift 5.5+
func fetchImages() async throws -> [UIImage] {
let (data, _) = try await URLSession.shared.data(from: url)
return parse(data)
}
Lợi ích: try-catch thay vì Result enum, linear flow, không retain cycle risk.
2. Await – Điểm Chờ (Suspension Point)
do {
// 1. Gọi async method - execution suspend tại đây
let images = try await fetchImages()
// 2. Resume khi fetchImages hoàn tất (success hoặc throw)
// 3. Gọi async method tiếp theo - tuần tự, dễ follow
let resized = try await resizeImages(images)
// 4. Resume
print("Fetched (images.count) images, resized to (resized.count)")
} catch {
// Bắt lỗi từ bất kỳ await nào trong do block
print("Error: (error)")
}
Key insight: `await` đánh dấu suspension point – thread có thể làm việc khác trong lúc chờ. Không block thread như sync code.

Structured Concurrency – Task Hierarchy & Cancellation
1. Structured vs Unstructured Tasks
// UNSTRUCTURED - Task detached, không tự cancel khi parent cancel
Task.detached {
await longRunningOperation() // Có thể leak, outlive parent
}
// STRUCTURED - Child task, tự động cancel khi parent cancel
func parent() async {
await withTaskGroup(of: Void.self) { group in
group.addTask { await child1() } // Child task
group.addTask { await child2() }
} // Tự động wait all children, cancel all nếu parent cancel
}
2. Task Groups – Parallel Execution An Toàn
func fetchAllUserData(userIds: [String]) async throws -> [UserData] {
try await withThrowingTaskGroup(of: UserData.self) { group in
for id in userIds {
group.addTask {
try await fetchUser(id: id) // Mỗi task chạy parallel
}
}
var results: [UserData] = []
for try await userData in group {
results.append(userData) // Collect results as they complete
}
return results
}
}
Ưu điểm: Automatic cancellation propagation, exception handling tập trung, backpressure tự nhiên.
3. Async Let – Parallel Fire-and-Forget
// SEQUENTIAL - chậm (total = sum of each)
let user = try await fetchUser(id: "1")
let posts = try await fetchPosts(userId: "1")
let friends = try await fetchFriends(userId: "1")
// PARALLEL với async let - nhanh (total = max of each)
async let user = fetchUser(id: "1")
async let posts = fetchPosts(userId: "1")
async let friends = fetchFriends(userId: "1")
let (userResult, postsResult, friendsResult) = try await (user, posts, friends)

Actors – Bảo Vệ Shared State Không Data Race
1. Actor Basics
// Actor = reference type với isolation built-in
actor ImageCache {
private var cache: [URL: UIImage] = [:]
private let maxSize = 100
// Method mặc định isolated - chỉ 1 task truy cập tại 1 thời điểm
func image(for url: URL) -> UIImage? {
cache[url]
}
func setImage(_ image: UIImage, for url: URL) {
if cache.count >= maxSize {
cache.removeFirst() // Simple LRU
}
cache[url] = image
}
// Nonisolated - có thể gọi từ non-async context (read-only safe)
nonisolated var count: Int { cache.count } // ⚠️ Chỉ dùng cho read-only computed property
}
// Usage - phải await vì actor-isolated
let cache = ImageCache()
let img = await cache.image(for: url)
await cache.setImage(newImage, for: url)
2. @MainActor – UI Updates An Toàn
// SwiftUI ViewModel - tất cả @Published updates trên MainActor
@MainActor
final class ContentViewModel: ObservableObject {
@Published var images: [UIImage] = []
@Published var isLoading = false
func loadImages() {
isLoading = true // MainActor - safe
Task { // Implicit @MainActor inheritance
do {
let fetched = try await ImageService.shared.fetchAll()
self.images = fetched // MainActor - safe UI update
} catch {
// handle error
}
self.isLoading = false
}
}
}
// Non-MainActor context gọi vào MainActor
func backgroundWork() {
Task { @MainActor in // Explicit MainActor hop
viewModel.loadImages()
}
}
3. Global Actors – Custom Isolation Domain
// Custom global actor cho database operations
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
// Sử dụng
@DatabaseActor
func saveUser(_ user: User) async throws { ... }
@DatabaseActor
func fetchUser(id: String) async throws -> User { ... }
// Call site - tự động hop to DatabaseActor
func syncUser() async {
try await saveUser(currentUser) // Runs on DatabaseActor
}
Sendable & Data Race Prevention – Swift 6 Strict Checking
1. Sendable Protocol
Type conform Sendable = an toàn để pass giữa concurrency domains (actors, tasks). Compiler kiểm tra:
- Value types (struct, enum): Tự động Sendable nếu tất cả stored properties Sendable
- Classes: Chỉ Sendable nếu final + immutable stored properties (let) + conform Sendable
- Actors: Tự động Sendable (isolation đảm bảo safety)
- Functions/@Sendable closures: Capture chỉ Sendable values
// ✅ Sendable - immutable value type
struct User: Sendable {
let id: String
let name: String
let email: String
}
// ❌ KHÔNG Sendable - mutable class
class UserCache {
var users: [String: User] = [:] // mutable stored property
}
// ✅ Sendable class - final + immutable
final class ImmutableConfig: Sendable {
let apiEndpoint: URL
let timeout: TimeInterval
init(endpoint: URL, timeout: TimeInterval) {
self.apiEndpoint = endpoint
self.timeout = timeout
}
}
// @Sendable closure - capture chỉ Sendable
func fetchData(completion: @Sendable (Result) -> Void) { ... }
2. @preconcurrency – Migration Gradual
Swift 6 strict checking có thể break existing code. Dùng @preconcurrency tạm thời:
// Import non-Sendable framework tạm thời
@preconcurrency import SomeLegacyFramework
// Hoặc attribute cho declaration
@preconcurrency
func legacyCallback(_ callback: @escaping (Result) -> Void) { ... }
AsyncSequence & AsyncStream – Streaming Data Async
1. AsyncSequence – Iterator Async
// Custom AsyncSequence cho pagination API
struct PaginatedUsers: AsyncSequence {
typealias Element = User
let apiClient: APIClient
let initialRequest: UserListRequest
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(client: apiClient, request: initialRequest)
}
struct AsyncIterator: AsyncIteratorProtocol {
var client: APIClient
var request: UserListRequest?
mutating func next() async throws -> User? {
guard let req = request else { return nil }
let response = try await client.fetchUsers(req)
request = response.nextPageRequest // nil nếu hết trang
return response.users.first // yield từng user
}
}
}
// Usage - for await loop tự động handle pagination
for try await user in PaginatedUsers(apiClient: client, initialRequest: .first) {
print(user.name) // Process each user as streamed
}
2. AsyncStream – Producer/Consumer Pattern
// Tạo stream từ callback-based API (NotificationCenter, delegate, etc.)
let notifications = AsyncStream { continuation in
let observer = NotificationCenter.default.addObserver(
forName: .dataDidChange, object: nil, queue: .main
) { notification in
continuation.yield(notification)
}
continuation.onTermination = { @Sendable _ in
NotificationCenter.default.removeObserver(observer)
}
}
// Consumer
for await notification in notifications {
await handleDataChange(notification)
}

Migration Strategy: Từ Legacy Codebase Sang Swift 6
Bước 1: Enable Strict Concurrency Checking (Xcode 16+)
Build Settings → SWIFT_STRICT_CONCURRENCY = complete (hoặc SWIFT_UPCOMING_FEATURE_StrictConcurrency cho gradual). Compiler sẽ báo tất cả data race potential.
Bước 2: Fix Warnings – Priority Cao Đến Thấp
- Non-Sendable capture in @Sendable closure: Dùng
@preconcurrencyhoặc refactor capture - Mutable state shared across actors: Bọc vào Actor hoặc dùng
Mutex(Swift 6.2+) - MainActor isolation violations: Thêm
@MainActorhoặcTask { @MainActor in } - Global mutable variables: Chuyển sang Actor hoặc
@MainActorstatic property
Bước 3: Refactor Completion Callbacks → Async
Xcode 16 cung cấp 3 refactor options (Editor → Refactor):
- Convert Function to Async: Thay thế hoàn toàn – dùng khi không cần backward compatibility
- Add Async Alternative: Giữ legacy + thêm async version với
@available(*, deprecated, renamed: "newAsync()")– khuyến nghị cho library/SDK - Add Async Wrapper: Async method mới gọi legacy completion-based qua
withCheckedThrowingContinuation– ít code thay đổi nhất
Bước 4: Adopt Actors Cho Shared State
Identify mọi class có mutable state được access từ multiple concurrency domains → chuyển thành actor.
Bước 5: Test Với Thread Sanitizer (TSan)
# Scheme → Diagnostics → Thread Sanitizer
# Chạy unit tests + UI tests
# TSan detect data race at runtime (complement compile-time checking)
Best Practices Production-Ready
| Pattern | Khuyến Nghị | Lý Do |
|---|---|---|
| Task creation | Ưu tiên Task { } structured, hạn chế Task.detached |
Structured = automatic cancellation, leak prevention |
| Cancellation | Check Task.isCancelled trong loop dài, respect CancellationError |
Tránh wasted work, resource leak |
| Timeout | Dùng withTaskTimeout (Swift 6.2+) hoặc custom wrapper |
Tránh hang forever trên network call |
| Retry/Backoff | Implement exponential backoff với Task.sleep (Swift 6.2: Task.sleep(for:)) |
Resilience cho network flaky |
| Testing | Dùng await fulfillment(of: [expectation], timeout:) cho async test |
Deterministic async testing |
| Debugging | Xcode 16: Debug navigator → Structured Concurrency view (task tree, suspension points) | Visualize task hierarchy, cancellation flow |
Common Pitfalls & Giải Pháp
1. “Async call in function that does not support concurrency”
// ❌ Sai - sync function gọi async
func viewDidLoad() {
let data = try await fetchData() // Error!
}
// ✅ Đúng - wrap trong Task @MainActor
func viewDidLoad() {
Task { @MainActor in
do {
let data = try await fetchData()
updateUI(with: data)
} catch { handleError(error) }
}
}
2. Capture non-Sendable trong @Sendable closure
// ❌ Sai - capture self (non-Sendable class)
Task { @Sendable in
self.updateUI() // self không Sendable
}
// ✅ Đúng - capture weak hoặc dùng Actor
Task { @MainActor [weak self] in
self?.updateUI() // weak self Sendable (optional)
}
// Hoặc: actor-isolated method
actor UIUpdater {
func update() { ... }
}
let updater = UIUpdater()
Task { await updater.update() } // Actor-isolated, safe
3. Forgetting to await – fire and forget bug
// ❌ Sai - không await, task chạy nền nhưng error bị swallow
func save() {
Task { try await database.save(data) } // Error bị mất!
}
// ✅ Đúng - handle error hoặc return Task
func save() async throws {
try await database.save(data)
}
// Hoặc explicit discard với error handling
func save() {
Task {
do { try await database.save(data) }
catch { logger.error("Save failed: (error)") }
}
}
Swift 6.2+ New Features (WWDC 2025)
- Default Actor Isolation: Non-isolated async functions default chạy trên caller’s actor (giảm hop overhead)
- @concurrent: Attribute cho phép function chạy concurrent an toàn (pure functions)
- Mutex & Synchronization Framework:
Mutexcho low-level sync (thay thếos_unfair_lock) - Task.sleep(for:) &
Task.sleep(until:) - duration-based API thay vì nanoseconds - Testing improvements:
await confirmation { },await expect { }cho Swift Testing framework
Kết Luận
Swift 6 Structured Concurrency không chỉ syntax mới - là paradigm shift từ manual thread management sang compiler-guaranteed safety. Investment migration trả lời: data race = 0 tại compile time, cancellation tự động, code đọc hiểu như synchronous. Best approach: gradual adoption với @preconcurrency, Xcode refactor tools, TSan validation. Kết quả: codebase an toàn, maintainable, sẵn sàng cho multi-core Apple Silicon (M3/M4) tận dụng tối đa.
Nguồn: Async await in Swift explained - Antoine van der Lee | Migrating to Swift 6 - SwiftLee | Swift Concurrency Documentation | Swift 6.2 Concurrency Changes
