Section 3/131 menit

3. Structured Concurrency

3. Structured Concurrency

Teori: Task Tree dan Structured Concurrency

Swift Concurrency menggunakan model structured concurrency — setiap Task memiliki parent, dan lifetime-nya terikat pada scope yang membuatnya. Ini mencegah "fire and forget" yang tidak terdeteksi.

swift
Task (root)
├── async let userTask
├── async let postsTask
└── withTaskGroup
    ├── subtask A
    ├── subtask B
    └── subtask C

Jika parent di-cancel atau selesai, semua child otomatis di-cancel.

Task {} vs Task.detached {}

swift
class ProfileViewController: UIViewController {

    // Task {} — mewarisi actor isolation dari konteks pemanggil
    // Di sini mewarisi @MainActor dari ViewController
    func loadProfile() {
        Task {
            // Berjalan di @MainActor context (inherited)
            let user = try await service.fetchUser(id: "1")
            // Langsung update UI, tidak perlu dispatch ke main
            self.nameLabel.text = user.name
        }
    }

    // Task.detached {} — TIDAK mewarisi isolation, berjalan di background
    // Gunakan hanya jika perlu explicitly lepas dari actor context
    func processInBackground() {
        Task.detached(priority: .background) {
            let result = await heavyComputation()
            // Harus kembali ke MainActor secara manual untuk update UI
            await MainActor.run {
                self.resultLabel.text = result
            }
        }
    }
}

Rekomendasi: Gunakan Task {} (bukan Task.detached) sebagai default. Task.detached hanya untuk kasus yang benar-benar perlu lepas dari isolation parent.

async let — Concurrent Binding

async let menjalankan beberapa operasi secara paralel tanpa TaskGroup:

swift
func loadDashboard() async throws -> Dashboard {
    // Ketiga fetch ini berjalan BERSAMAAN, bukan berurutan
    async let user = service.fetchUser(id: currentUserId)
    async let posts = service.fetchPosts(userId: currentUserId)
    async let config = service.fetchAppConfig()

    // await semua sekaligus — total waktu = waktu terlama dari ketiganya
    let (u, p, c) = try await (user, posts, config)

    return Dashboard(user: u, posts: p, config: c)
}

// Bandingkan dengan sequential — LEBIH LAMBAT
func loadDashboardSlow() async throws -> Dashboard {
    let user = try await service.fetchUser(id: currentUserId)   // tunggu selesai
    let posts = try await service.fetchPosts(userId: currentUserId) // baru mulai
    let config = try await service.fetchAppConfig()              // baru mulai
    return Dashboard(user: user, posts: posts, config: config)
}

withTaskGroup — Concurrency Dinamis

Ketika jumlah task tidak diketahui saat compile time:

swift
func fetchAllUsers(ids: [String]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        // Tambahkan task secara dinamis
        for id in ids {
            group.addTask {
                try await service.fetchUser(id: id)
            }
        }

        // Kumpulkan semua hasil
        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

Cancellation

swift
func longRunningOperation() async throws -> [Result] {
    var results: [Result] = []

    for item in largeDataset {
        // Cek cancellation di setiap iterasi
        try Task.checkCancellation()  // throws CancellationError jika cancelled

        // Atau cek secara manual
        if Task.isCancelled {
            return results  // return partial result
        }

        let result = try await processItem(item)
        results.append(result)
    }

    return results
}

// Simpan referensi Task untuk bisa cancel nanti
class SearchViewController: UIViewController {
    private var searchTask: Task<Void, Never>?

    func search(query: String) {
        // Cancel search sebelumnya jika masih berjalan
        searchTask?.cancel()

        searchTask = Task {
            do {
                let results = try await searchService.search(query: query)
                self.displayResults(results)
            } catch is CancellationError {
                // Task di-cancel, abaikan — tidak perlu show error ke user
                return
            } catch {
                self.showError(error)
            }
        }
    }
}