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.