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:
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:
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:
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
@escapingclosure
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."
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 aliasing → tidak ada race
(kedua)
bisa mutasi → RACE!
4.3 Apa yang Membuat Sebuah Tipe "Sendable"?
Sebuah tipe aman untuk di-send jika dan hanya jika kita bisa menjamin salah satu dari:
- Immutable — tidak bisa di-mutate, tidak ada race (misalnya
letproperties saja) - Copied — value semantics, setiap domain punya salinannya sendiri
- Internally synchronized — punya mekanisme internal untuk mencegah concurrent mutation (misalnya actor, lock)
- 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)
// 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
// 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
// @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 Sendablemenonaktifkan 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:
// 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:
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:
// ❌ 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 implisitTask.detached { }— selalu @Sendable@escapingclosure yang dikirim ke thread lain- Argumen ke concurrent functions
4.6 Sendable di Protocol — Implikasi Penting
// 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