Table of Contents7 sections
async/await đã thay đổi cách chúng ta viết code bất đồng bộ trong Swift. Callback hell biến mất, luồng logic đọc từ trên xuống như code đồng bộ, và error handling gọn gàng nhờ try. Nhưng sau vài dự án production, tôi nhận ra một điều: viết async/await là phần dễ, còn quản lý vòng đời của Task mới là nơi phân định giữa một codebase ổn định và một codebase rò rỉ tài nguyên âm thầm.
1. Task Cancellation: Cooperative, không phải Preemptive
Điểm cốt lõi cần hiểu trước tiên: cancellation trong Swift Concurrency là cooperative. Khi bạn gọi task.cancel(), runtime không kill task. Nó chỉ set một cờ isCancelled = true. Task vẫn tiếp tục chạy và tiêu tốn CPU, network, memory — trừ khi chính code của bạn chủ động kiểm tra cờ đó và dừng lại.
Điều này khác hoàn toàn với việc hệ điều hành terminate một tiến trình. Trách nhiệm dừng đúng lúc thuộc về người viết code.
Nhiều API hệ thống đã tự cooperative
Một hiểu lầm phổ biến là phải rải try Task.checkCancellation() khắp nơi. Thực tế, phần lớn các API async của Apple đã tự kiểm tra cancellation và ném CancellationError giúp bạn:
URLSession.data(for:)— hủy request khi task bị cancel.Task.sleep(...)— ném lỗi ngay khi bị cancel.Các operator trong structured concurrency (
TaskGroup,async let).
Vì vậy, checkCancellation() chỉ thực sự cần thiết ở hai chỗ:
Vòng lặp nặng thuần CPU (parse dữ liệu lớn, xử lý ảnh, tính toán trong loop) — nơi không có điểm
awaitnào để runtime tự kiểm tra.Trước một side-effect không thể hoàn tác (ghi database, cập nhật UI) — để tránh commit kết quả của một task đã lỗi thời.
Gọi checkCancellation() một cách máy móc sau mỗi await URLSession là dư thừa và làm code nhiễu.
Pattern chuẩn: hủy task cũ trước khi tạo task mới

Đây là kịch bản kinh điển: User pull-to-refresh liên tục, hoặc chuyển tab qua lại. Mỗi lần load, ta phải hủy request đang chạy dở để tránh race condition khi hai response về không đúng thứ tự.
@MainActor
final class FeedViewModel: ObservableObject {
@Published private(set) var feed: [FeedItem] = []
@Published private(set) var state: LoadState = .idle
private let networkService: FeedServicing
private var fetchTask: Task<Void, Never>?
init(networkService: FeedServicing) {
self.networkService = networkService
}
func loadData() {
// Hủy task cũ. Task mới sẽ là source of truth duy nhất.
fetchTask?.cancel()
fetchTask = Task {
state = .loading
do {
let data = try await networkService.fetchFeed()
// Sau await, task cũ có thể đã bị cancel bởi một loadData() mới.
// checkCancellation() ở đây là hàng rào rõ ràng trước khi ghi vào @Published state.
try Task.checkCancellation()
feed = data
state = .loaded
} catch is CancellationError {
// Không đổi state: một task khác đang chịu trách nhiệm.
return
} catch {
state = .failed(error)
}
}
}
deinit {
// Bắt buộc với class do mình quản lý vòng đời:
// tránh task giữ self sống sau khi object lẽ ra đã được giải phóng.
fetchTask?.cancel()
}
}
Cơ chế bảo vệ ở đây quan trọng và đáng giải thích rõ: khi loadData() được gọi lần hai, nó cancel task thứ nhất. Task thứ nhất — nếu đang await — sẽ ném CancellationError và đi vào nhánh catch is CancellationError, nơi ta cố tình không làm gì. Nhờ vậy dữ liệu cũ không bao giờ ghi đè dữ liệu mới. Việc @MainActor serialize mọi truy cập vào feed trên main thread loại bỏ nốt phần race condition còn lại.
2. Task Priority: Tránh Priority Inversion, tin vào Escalation

iOS phân bổ CPU dựa trên priority. Gán sai priority có thể gây priority inversion — một tác vụ UI quan trọng phải xếp hàng chờ sau một tác vụ nền, dẫn đến giật lag.
Các mức priority chính:
.userInitiated— người dùng đang trực tiếp chờ kết quả (pull-to-refresh, mở chi tiết một item)..utility— tác vụ dài mà người dùng biết đang chạy ngầm, thường có progress indicator (tải file lớn)..background— tác vụ hoàn toàn ngầm, không ảnh hưởng trải nghiệm hiện tại (prefetch trang kế, đồng bộ log analytics).
Task(priority: .userInitiated) {
await performCriticalUpdate()
}
Task(priority: .background) {
await prefetchNextPageImages()
}
Điểm mà nhiều tài liệu bỏ qua: Priority Escalation
Swift Concurrency xử lý priority inversion tốt hơn GCD nhờ priority escalation. Khi một task priority cao await kết quả của một task priority thấp hơn, runtime tự động nâng priority của task thấp lên tạm thời cho đến khi nó hoàn thành. Nghĩa là bạn ít khi phải lo tình huống một background task chặn một user-initiated task một cách vô hạn.
3. Task Timeout
Swift Concurrency, vẫn chưa có .timeout() sẵn cho Task. Nếu một endpoint bị treo, task có thể kẹt ở await rất lâu.
Để giải quyết vấn đề này, chúng ta cần một Pattern kết hợp giữa withThrowingTaskGroup và Task.sleep

struct TimeoutError: Error, LocalizedError {
let seconds: TimeInterval
var errorDescription: String? {
"Operation timed out after \(seconds)s"
}
}
/// Chạy một operation async với giới hạn thời gian.
/// Đây là hàm global, KHÔNG phải extension trên Task —
/// vì Task đã có generic params riêng, đặt vào đó sẽ nhập nhằng.
func withTimeout<T: Sendable>(
seconds: TimeInterval,
operation: @escaping @Sendable () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
// Task 1: công việc thật.
group.addTask {
try await operation()
}
// Task 2: đồng hồ đếm ngược.
group.addTask {
try await Task.sleep(for: .seconds(seconds))
throw TimeoutError(seconds: seconds)
}
// Chờ task đầu tiên hoàn thành (thành công hoặc ném lỗi).
let result = try await group.next()!
// Hủy task còn lại ngay để giải phóng tài nguyên.
group.cancelAll()
return result
}
}
Cách dùng:
func fetchUserProfile() async {
do {
let profile = try await withTimeout(seconds: 5) {
try await apiService.getUserProfile()
}
// Xử lý profile...
} catch is TimeoutError {
// Hiển thị màn hình lỗi mạng, gợi ý thử lại.
} catch is CancellationError {
// Một Task khác đang thực hiện, bỏ qua.
} catch {
// Lỗi khác.
}
}
Tổng kết
Áp dụng Swift Concurrency không dừng lại ở việc thay Completion Handler bằng async/await. Sự khác biệt giữa một Data Layer bền vững và một Data Layer rò rỉ nằm ở chỗ bạn có kiểm soát được vòng đời Task hay không.
