Section 8/171 menit
8. Best Practice: Task Lifecycle & Structured Concurrency
8. Best Practice: Task Lifecycle & Structured Concurrency
BP 8.1: Prefer async let & TaskGroup daripada Task
Task { } membuat task unstructured — tidak ada parent-child. Cancellation tidak otomatis propagate. async let dan TaskGroup punya struktur jelas.
swift
// ❌ Unstructured: kalau caller di-cancel, child tidak otomatis cancel
func loadHomeScreen() async throws {
Task { try await self.refreshUser() }
Task { try await self.refreshFeed() }
}
// ✓ Structured: cancellation otomatis propagate
func loadHomeScreen() async throws {
async let user = refreshUser()
async let feed = refreshFeed()
_ = try await (user, feed)
}
// ✓ Untuk N task dinamis
func refreshMany(ids: [UUID]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask { try await fetchUser(id) }
}
var result: [User] = []
for try await user in group {
result.append(user)
}
return result
}
}
BP 8.2: Selalu Simpan Reference Task Long-Running
Task yang tidak di-await dan tidak di-store akan terus jalan walau caller "selesai". Untuk task yang hidup selama lifecycle object, simpan referensinya.
swift
@MainActor
final class LiveViewModel {
var feed: [Post] = []
private var subscription: Task<Void, Never>?
func startListening() {
subscription?.cancel()
subscription = Task { [weak self] in
for await update in feedStream {
self?.feed.append(update)
}
}
}
func stopListening() {
subscription?.cancel()
subscription = nil
}
deinit {
subscription?.cancel() // pastikan ter-clean saat object dilepas
}
}
BP 8.3: Cancel Sebelum Replace
Pattern umum di ViewModel: query yang berubah → cancel query lama → start query baru. Ini debouncing alami.
swift
@MainActor
final class SearchViewModel {
var results: [Item] = []
private var currentTask: Task<Void, Never>?
func setQuery(_ q: String) {
currentTask?.cancel() // cancel sebelum start baru
currentTask = Task { [weak self] in
guard let self else { return }
do {
let found = try await search.execute(q)
guard !Task.isCancelled else { return }
self.results = found
} catch is CancellationError {
// expected
} catch {
// handle real errors
}
}
}
}
BP 8.4: Check Task.isCancelled di Loop Panjang
Cancellation di Swift bersifat cooperative — kalau kode tidak cek, ia akan terus jalan.
swift
// ❌ Ignore cancellation
for item in bigList {
await process(item)
}
// ✓ Respect cancellation
for item in bigList {
try Task.checkCancellation() // throws CancellationError kalau di-cancel
await process(item)
}
// ✓ Atau pakai isCancelled untuk early return non-throwing
for item in bigList {
if Task.isCancelled { return }
await process(item)
}
BP 8.5: Detached Hanya untuk Kasus yang Benar-Benar Lepas
Task.detached melepas isolation, priority, dan task local dari parent. Hanya pakai kalau memang itu yang kamu inginkan.
swift
// ❌ detached saat sebenarnya butuh struktur
@MainActor
func reload() {
Task.detached { // kenapa detached?
let data = try await api.fetch()
await MainActor.run { self.data = data } // hop balik manual
}
}
// ✓ Tanpa detached: lebih sederhana
@MainActor
func reload() {
Task {
let data = try await api.fetch() // hop ke actor api
self.data = data // sudah balik di MainActor karena task inherits
}
}
Kapan detached valid:
- Fire-and-forget logging.
- Background work yang tidak boleh di-cancel saat caller di-cancel.
- Operasi yang butuh priority spesifik yang berbeda dari caller.