Section 4/133 menit

4. Sendable — Mengapa Ada dan Cara Kerjanya

4. Sendable — Mengapa Ada dan Cara Kerjanya

4.1 Akar Masalah: Value Semantics vs Reference Semantics

Ini adalah inti dari seluruh konsep Sendable. Pahami ini, dan selebihnya akan masuk akal sendiri.

Value Semantics (struct, enum, tuple)

Ketika kamu meng-assign atau meneruskan value type, Swift membuat salinan independen:

swift
var a = Point(x: 1, y: 2)
var b = a           // b adalah SALINAN — alamat memori berbeda
b.x = 10

print(a.x)  // masih 1 — a tidak terpengaruh
print(b.x)  // 10

Karena setiap Task/thread punya salinannya sendiri, tidak ada yang "berbagi" memori. Tidak ada yang bisa race condition — tidak ada shared state. Ini kenapa struct dan enum secara alami thread-safe.

Reference Semantics (class)

Ketika kamu meng-assign atau meneruskan reference type, kamu meneruskan pointer ke objek yang sama:

swift
class Counter { var count = 0 }

let a = Counter()
let b = a           // b menunjuk ke OBJEK YANG SAMA — alamat memori sama!
b.count = 10

print(a.count)  // 10 — a berubah! karena a dan b adalah objek yang sama

Sekarang bayangkan ini terjadi lintas thread:

swift
let counter = Counter()  // satu objek di heap

Task {
    counter.count += 1   // Thread 1 menulis
}
Task {
    counter.count += 1   // Thread 2 juga menulis ke lokasi yang SAMA
}
// → Data race!

Inilah masalah mendasarnya: Reference semantics membuat aliasing — beberapa variabel menunjuk ke satu lokasi memori — dan ketika ada mutation yang terjadi concurrent, terjadilah data race.

4.2 Mengapa Swift Membutuhkan Sendable

Swift butuh cara untuk menyatakan ke compiler: "nilai ini aman untuk dikirim ke isolation domain lain."

Dalam dunia actor, "mengirim nilai ke isolation domain lain" artinya:

  • Melewatkan argumen ke method actor dari luar actor
  • Mengembalikan nilai dari actor ke caller
  • Memasukkan nilai ke dalam Task {}
  • Meneruskan nilai sebagai @escaping closure

Jika nilai yang dikirim adalah sebuah class (reference type) yang memiliki mutable state, maka setelah pengiriman, dua isolation domain berbeda memegang referensi ke objek yang sama — dan keduanya bisa mutasi objek itu concurrently. Data race.

Sendable adalah promise ke compiler: "nilai bertipe ini tidak akan menyebabkan data race ketika dikirim lintas isolation boundary."

swift
Tanpa Sendable:           Dengan Sendable:
                          
Actor A ──── class ────▶ Actor B    Actor A ──── struct ────▶ Actor B
              │                                   (copy)
              ▼                     
         [shared object]            Actor A: punya salinannya
         ↑           ↑             Actor B: punya salinannya
    Actor A      Actor B           Tidak ada aliasingtidak ada race
    (kedua)
    bisa mutasiRACE!

4.3 Apa yang Membuat Sebuah Tipe "Sendable"?

Sebuah tipe aman untuk di-send jika dan hanya jika kita bisa menjamin salah satu dari:

  1. Immutable — tidak bisa di-mutate, tidak ada race (misalnya let properties saja)
  2. Copied — value semantics, setiap domain punya salinannya sendiri
  3. Internally synchronized — punya mekanisme internal untuk mencegah concurrent mutation (misalnya actor, lock)
  4. Never shared — tidak mungkin diakses concurrent (misalnya value yang hanya hidup dalam satu Task)

Swift mengkodekan ini ke dalam beberapa kategori:

4.4 Kategori Sendable dan Kapan Digunakan

Kategori 1: Automatic Sendable (tidak perlu deklarasi)

swift
// Semua ini otomatis Sendable karena Swift bisa membuktikannya:

// Primitif: immutable dan copied
let n: Int = 42           // Sendable
let s: String = "hello"   // Sendable
let b: Bool = true        // Sendable

// Struct: value semantics — setiap assignment adalah copy
struct Point { var x: Double; var y: Double }
// Sendable otomatis karena:
// - x adalah Double (Sendable)
// - y adalah Double (Sendable)
// - struct memberikan copy semantics
// → tidak ada aliasing yang mungkin

// Enum: sama seperti struct
enum Status { case active, inactive, pending }  // Sendable otomatis

// Collection dari Sendable juga Sendable
let numbers: [Int] = [1, 2, 3]       // Array<Int> adalah Sendable
let dict: [String: Int] = [:]         // Dictionary<String, Int> adalah Sendable

Kategori 2: Explicit Sendable untuk Struct/Class yang Kompleks

swift
// Struct dengan semua Sendable properties → otomatis, tapi bisa eksplisit
struct Message: Sendable {
    let id: UUID        // Sendable
    let content: String // Sendable
    let timestamp: Date // Sendable
}

// Actor — selalu Sendable (protected by design)
actor DataStore: Sendable {  // 'Sendable' di sini redundant tapi OK
    var items: [String] = []
}

// Final class dengan hanya immutable properties
final class ImmutableConfig: Sendable {
    let timeout: Int      // let — tidak bisa diubah setelah init
    let baseURL: URL      // let — immutable
    
    init(timeout: Int, baseURL: URL) {
        self.timeout = timeout
        self.baseURL = baseURL
    }
}

// ❌ Ini TIDAK bisa Sendable:
// final class MutableConfig: Sendable {  // ERROR
//     var timeout: Int  // var + class = bisa concurrent mutation
// }

Kategori 3: @unchecked Sendable — Escape Hatch yang Berbahaya

swift
// @unchecked Sendable: "saya tahu ini aman, tapi compiler tidak bisa membuktikannya"
// Gunakan HANYA ketika kamu punya mekanisme synchronization manual yang benar

final class ThreadSafeCache: @unchecked Sendable {
    private var store: [String: Any] = [:]
    private let lock = NSLock()            // manual synchronization
    
    func set(_ value: Any, for key: String) {
        lock.withLock { store[key] = value }  // setiap akses dilindungi lock
    }
    
    func get(for key: String) -> Any? {
        lock.withLock { store[key] }
    }
}

Peringatan keras: @unchecked Sendable menonaktifkan SEMUA pemeriksaan compiler untuk tipe ini. Kesalahan kecil di dalam implementasi (lupa lock satu akses) bisa menyebabkan data race yang sulit dideteksi. Gunakan sebagai last resort, dan dokumentasikan dengan jelas mengapa aman.

Kapan terpaksa @unchecked Sendable:

  • Wrapping library C/Objective-C yang thread-safe tapi tidak diketahui compiler
  • Legacy code yang sudah terbukti aman secara audit tapi tidak bisa direfactor sekarang
  • Tipe dengan invariant keamanan yang tidak bisa diekspresikan dalam type system

Alternatif yang lebih baik sebelum @unchecked Sendable:

swift
// Opsi 1: jadikan actor — compiler membuktikan keamanannya
actor Cache {
    private var store: [String: Any] = [:]
    func set(_ value: Any, for key: String) { store[key] = value }
    func get(for key: String) -> Any? { store[key] }
}

// Opsi 2: jadikan struct (jika value semantics cukup)
struct Configuration: Sendable {
    var timeout: Int = 30
    // struct copy semantics → tidak ada shared mutable state
}

4.5 @Sendable Closure — Mengapa Closure Spesial

Closure memiliki kemampuan untuk capture variable dari lingkup sekitarnya. Ini yang membuatnya berbeda dari function biasa:

swift
var counter = 0

let closure = {
    counter += 1  // closure ini "capture" counter
}

Jika closure ini dikirim ke thread lain, dan counter juga diakses dari thread asal, terjadilah data race pada counter.

@Sendable closure berjanji: closure ini tidak menyimpan reference ke mutable state yang bisa di-race:

swift
// ❌ Tidak bisa @Sendable: meng-capture mutable var
var mutableState = 0
Task { @Sendable in
    mutableState += 1  // ERROR: capture of 'mutableState' with non-sendable type
}

// ✅ @Sendable OK: capture immutable state
let immutableState = 42
Task { @Sendable in
    print(immutableState)  // OK: immutable, tidak ada race
}

// ✅ @Sendable OK: capture Sendable reference (actor)
actor Store { var items: [String] = [] }
let store = Store()
Task { @Sendable in
    await store.items.append("new")  // OK: actor melindungi state-nya
}

Kapan butuh @Sendable closure:

  • Task { } — selalu @Sendable secara implisit
  • Task.detached { } — selalu @Sendable
  • @escaping closure yang dikirim ke thread lain
  • Argumen ke concurrent functions

4.6 Sendable di Protocol — Implikasi Penting

swift
// Protocol Sendable: semua conforming type harus Sendable
protocol DataProcessor: Sendable {
    func process(_ data: Data) -> Result<String, Error>
}

// Ini berguna ketika protocol digunakan sebagai existential dalam actor
actor Pipeline {
    var processors: [any DataProcessor] = []  // OK: DataProcessor adalah Sendable
    
    func addProcessor(_ processor: any DataProcessor) {
        processors.append(processor)  // aman: processor adalah Sendable
    }
}

Kapan protocol perlu Sendable:

  • Ketika existential (any Protocol) akan disimpan di actor sebagai property
  • Ketika implementasi protocol akan dikirim lintas isolation boundary