Section 7/132 menit

7. Isolation + Async/Await

7. Isolation + Async/Await

7.1 Teori: Executor dan Context Switching

Swift's async/await tidak menggunakan thread-per-task seperti Grand Central Dispatch. Sebaliknya, ia menggunakan cooperative multitasking dengan executor:

  • Setiap isolation domain (actor) punya executor (mirip serial queue, tapi lebih efisien)
  • Sebuah Task berjalan pada executor tertentu pada satu waktu
  • Ketika sebuah Task melakukan await, ia melepaskan executor-nya (bukan memblokir thread)
  • Thread yang dibebaskan bisa langsung mengerjakan Task lain
  • Ketika await selesai, Task dijadwalkan ulang ke executor yang sesuai
swift
Thread Pool (misalnya 4 core):
┌────────┬────────┬────────┬────────┐
│Thread 1Thread 2Thread 3Thread 4│
└───┬────┴───┬────┴───┬────┴────────┘
    │        │        │
    │ Task ATask BTask C
    │ (@Main)│ (actor)│ (nonisolated)
    │        │        │
    ▼await   │        │
    [suspend]│        │         ← Thread 1 bebas!
             │        │
    Thread 1 mengerjakan Task D sementara Task A menunggu

Ini jauh lebih efisien dari "satu thread per request" karena satu thread bisa mengerjakan ratusan Tasks yang saling bergantian.

7.2 Isolation Propagation

Ketika sebuah async function dipanggil, ia mewarisi isolation dari caller-nya — kecuali function itu sendiri punya anotasi isolation berbeda:

swift
@MainActor
func updateUI(with text: String) async {
    label.text = text           // di main thread (diwarisi dari @MainActor)
    
    let processed = await processText(text)  // hop ke nonisolated context
    
    label.text = processed      // kembali ke main thread (kembali ke @MainActor)
}

// Fungsi ini TIDAK diisolasi — berjalan di background cooperative pool
func processText(_ text: String) async -> String {
    // heavy processing di background
    return text.uppercased()
}

Aturan propagasi:

  1. Jika caller @MainActor dan callee tidak punya anotasi → callee berjalan di background, caller await dan kembali ke main
  2. Jika caller adalah actor dan callee adalah actor lain → hop terjadi, butuh await
  3. Jika caller dan callee sama isolation domain → tidak ada hop, tidak ada overhead

7.3 Task dan Isolation

Task mewarisi isolation dari konteks pembuatannya:

swift
@MainActor
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Task ini MEWARISI @MainActor isolation dari ViewController
        Task {
            // Berada di @MainActor — aman untuk UI update
            let data = await fetchData()  // hop ke background sementara
            updateUI(with: data)          // kembali ke @MainActor — aman
        }
        
        // Task.detached TIDAK mewarisi isolation — berjalan di pool umum
        Task.detached {
            // BUKAN @MainActor — tidak aman untuk UI update langsung
            let data = await self.fetchData()
            await MainActor.run {
                self.updateUI(with: data)  // harus eksplisit
            }
        }
    }
}

Gunakan Task.detached hanya ketika:

  • Kamu secara eksplisit tidak ingin mewarisi isolation parent
  • Task benar-benar independent dan tidak boleh terpengaruh cancellation dari parent
  • Kamu ingin prioritas eksekusi berbeda dari parent

Default: gunakan Task {} biasa — lebih aman, kode lebih bersih.

7.4 Melintasi Isolation Boundary

Setiap crossing isolation boundary memerlukan await:

swift
actor DataStore {
    var items: [String] = []
    
    func addItem(_ item: String) {
        items.append(item)
    }
}

@MainActor
func addAndDisplay(item: String, to store: DataStore) async {
    // Crossing: @MainActor → DataStore actor
    await store.addItem(item)            // hop diperlukan
    
    let count = await store.items.count  // hop lagi
    label.text = "Total: \(count)"       // kembali ke @MainActor
}

Visualisasi boundary crossing:

swift
@MainActor                    DataStore actor
─────────────────────         ──────────────────
addAndDisplay() starts
                   ─ await store.addItem() ─▶  addItem() runs
(suspended)                                     items.append()
                   ◀─────────────────────── done
resumes
label.text = ...

7.5 withIsolation dan assumeIsolated

swift
// assumeIsolated: "saya yakin kita sudah di isolation domain ini"
// Berguna ketika kamu memanggil dari callback yang compiler tidak bisa trace
actor MyActor {
    var value = 0
    
    func doSomething() {
        // Jalankan closure secara synchronous dalam isolation domain ini
        self.assumeIsolated { isolatedSelf in
            isolatedSelf.value += 1  // OK karena kita sudah dalam domain
        }
    }
    
    // Contoh nyata: delegate callback dari library lama
    func setupCallback() {
        SomeLegacyLibrary.onEvent = { [weak self] in
            // Library ini memanggil di thread yang tidak diketahui
            // Kita yakin kita harus handle ini dalam actor
            self?.assumeIsolated { actor in
                actor.handleEvent()
            }
        }
    }
    
    private func handleEvent() {
        value += 1
    }
}

Kapan assumeIsolated:

  • Bridging dari callback-based API ke actor
  • Ketika kamu yakin (melalui dokumentasi atau code audit) bahwa kamu sudah dalam isolation domain yang benar
  • JANGAN gunakan untuk menghindari compiler error jika kamu tidak yakin