Yunus Emre Alpak

İzole Et ya da Güvenle Paylaş — Dart ve Swift'in İki Eşzamanlılık Felsefesi

2026 · 07 · 189 dk okuma

Flutter'da yıllarca mutex yazmadan yaşadım. Swift'e geçince daha ilk gün DispatchQueue.main.async ile, ardından actor ve Sendable ile tanıştım. Meğer iki dil aynı düşmana — data race'e — zıt iki felsefeyle saldırıyormuş: Dart paylaşmayı yasaklar, Swift paylaşımı güvenli kılar. İşte Flutter'dan iOS'a geçenin thread haritası.

İki dil, aynı düşman

Eşzamanlı programlamanın baş belası data race'tir (veri yarışı): iki thread'in aynı belleğe, en az biri yazarak, aynı anda dokunması. Sonucu bozuk veri ve açıklanamayan çökmelerdir. Dart ve Swift bu tek soruna taban tabana zıt iki cevap verir — ve bu iki cevabı anlamak, Flutter'dan iOS'a geçerken kafandaki "thread" boşluğunu tam olarak dolduran şeydir.

Kısaca: Dart paylaşmayı yasaklar, Swift paylaşımı güvenli kılar. Gerisi bu cümlenin açılımı.

Dart tarafı: tek thread, bir event loop

Bir Dart isolate'i (yalıtılmış çalışma birimi) resmî tanımıyla şudur: kendi özel belleği (private heap) ve bir event loop çalıştıran tek bir thread. Tüm Dart kodu bir isolate içinde koşar; program varsayılan main isolate ile başlar ve çoğu uygulama başka bir isolate hiç açmaz.

O tek thread'in üstünde bir event loop döner — Flutter'ın akıcılığının altındaki mekanizma. İki kuyruğu vardır:

Kural yalın: loop her event'ten sonra microtask kuyruğunu tamamen boşaltır, ancak ondan sonra event kuyruğundaki bir sonraki olaya geçer. Yani async/await kodunu paralel çalıştırmaz; sadece bekleme anında sırayı bırakıp thread'i başka işe verir. Tek thread, sırayla, ama asla bloklamadan.

Paralellik nerede? İşte isolate'ler

Tek thread tek çekirdek demektir. Gerçekten paralel iş için — büyük bir JSON parse'ı, görüntü işleme — Dart yeni bir isolate açar. Ve Swift'ten en keskin ayrıldığı yer burasıdır:

İki isolate birbirinin belleğini göremez. Paylaşılan mutable state yoktur. Tek iletişim yolu mesajlaşmadır (message passing).

İletişim SendPort/ReceivePort ile olur ve ilişki asimetriktir: bir ReceivePort'u birçok SendPort besleyebilir. Dart'ın modeli aslında Actor model'in bir uygulamasıdır — durumu paylaşmayıp mesajla konuşan bağımsız aktörler. Flutter'da yıllarca mutex yazmamamın sebebi buydu: paylaşılan bellek yoksa, kilitlenecek bir şey de yok.

Modern API Isolate.run()'dır:

// Ağır işi arka plan isolate'ine taşı, sonucu geri al
final result = await Isolate.run(() => parseHugeJson(bytes));

Küçük bir tarih düzeltmesi: Isolate.run çoğu kez "Dart 3 yeniliği" diye anılır ama aslında Dart 2.19'da (Ocak 2023, Flutter 3.7) geldi — Dart 3'ten hemen önce. Tüm isolate yaşam döngüsünü (aç, çalıştır, sonucu döndür, kapat) tek çağrıda yönetir. Flutter'ın compute()'u mobil/masaüstünde kabaca Isolate.run'a denktir; web'de ise isolate yoktur, compute işi main thread'de koşturur.

İnce bir detay: sonuç geri kopyalanmaz, Isolate.exit ile taşınır (transfer/move) — mesajlaşma modeline rağmen sonuç dönüşü bir kopya değildir.

"Dart'ta data race olamaz" — yarı doğru

Burada dürüst olmak gerek, çünkü çok tekrarlanan bir yarı-gerçek var. Ayrı isolate'ler arasında paylaşılan bellek olmadığı için düşük seviye data race gerçekten imkânsızdır — tasarım gereği. Ama resmî Dart dokümanının kendisi, o meşhur cümlenin hemen ardından uyarır: "That said, isolates don't prevent race conditions all together."

Yani tek bir isolate içinde bile async araya girmeleriyle mantıksal race condition yaşayabilirsin: iki await arasında paylaşılan bir değişkenin değeri değişebilir. Nitekim package:mutex'in dokümanı bunu açıkça söyler: "Although Dart uses a single thread of execution, race conditions can still occur when asynchronous operations are used inside critical sections." Dart'ta bile kilit paketlerinin var olması tesadüf değil. Üstüne, dart:ffi ile ayrılan native bellek isolate'ler arasında paylaşılabilir ve orada yarış mümkündür.

Doğru cümle şu: Dart, paylaşılan-bellek data race'ini yapısal olarak eler — ama tüm yarış durumlarını değil.

Swift tarafı: gerçek çok-thread'li bir havuz

Swift'in dünyası tam tersi kurulu: paylaşılan bellekli ve gerçekten çok-thread'li.

Önce klasik katman, GCD (Grand Central Dispatch): işleri kuyruklara (DispatchQueue) atarsın, GCD bunları bir thread pool üstünde çalıştırır. Sorun şu: bir thread bloklanıp kuyrukta iş kalınca GCD yeni thread'ler açar ve çekirdek sayısının çok üstüne çıkabilir. Buna thread explosion denir — bellek şişer, context switch maliyeti artar.

Modern katman, Swift Concurrency (async/await, actor): altında bir cooperative thread pool vardır ve tek bir sözleşmesi vardır — thread sayısı çekirdek sayısını geçmez. Thread explosion'ı kökten çözer.

Dart'la en sert kontrast da burada. Swift'te await thread'i bloklamaz, askıya alır (suspend); devam bir continuation olarak heap'e konur, thread serbest kalır. Ama askıdan sonra kod aynı thread'de devam etmeyebilir. Dart'ta ise bir isolate'in tüm kodu daima o isolate'in tek thread'inde döner.

Dart: "async'in her yeri hep aynı thread." Swift: "await'ten sonra hangi thread — garanti yok."

Pratik sonucu: Swift'te bir kilidi await üzerinden tutamazsın, thread-local veri await'i aşamaz. (Bu makinenin iç işleyişini "await Satırının Altında" yazısında ayrıntılı açmıştım.)

Swift paylaşımı nasıl güvenli kılıyor: actor + Sendable

Paylaşılan bellek varsa, data race nasıl engelleniyor? Cevap Dart'ınkinin tam zıddı: paylaşımı yasaklamaz, derleyiciye ispatlatır.

@MainActor
final class ProfileViewModel {
    var name = ""            // yalnızca main thread'de erişilir — derleyici garantiler
}

actor ImageCache {
    private var store: [URL: Data] = [:]   // yarışa karşı izole
    func insert(_ d: Data, for url: URL) { store[url] = d }
}

Bir tuzak: actor'lar reentrant'tır. Sen bir await'te askıdayken aynı actor'a başka bir çağrı girebilir; await öncesi doğruladığın bir varsayım sonrasında bozulmuş olabilir. Yani actor'lar düşük seviye data race'i eler ama yüksek seviye race condition'ı elemez — tıpkı Dart'ın tek-isolate içi durumu gibi. İki dil, tam da o sınırda buluşur.

Küçük bir davranış farkı: actor'ın executor'ı serial bir DispatchQueue'ya benzer ama FIFO değildir — önceliğe göre çalışır (priority inversion'ı önlemek için). GCD serial queue ise katı FIFO'dur.

İki felsefe birbirine yaklaşıyor: Swift 6 → 6.2

Swift 6 "strict concurrency" ile derleme-zamanı data-race güvenliğini zorunlu kıldı. Ama bir sürtünme doğdu: dil, işaretlenmemiş her şeyi "eşzamanlı kullanılabilir" (nonisolated) varsayıyordu; basit, tek-thread'li bir program yazmak bile anotasyon yağmuru ve false-positive uyarılar gerektirebiliyordu.

Swift 6.2 (15 Eylül 2025) buna "approachable concurrency" ile karşılık verdi. İki kilit değişiklik:

Fark ettin mi? Swift, Dart'ın on yıldır yaptığı yere doğru yürüyor: varsayılan tek thread, paralellik açık talep. Dart oraya izolasyonla baştan varmıştı; Swift paylaşımlı modelden aynı ergonomiye evriliyor. Aynı zirveye iki farklı yamaçtan.

"Flutter tek thread'lidir" efsanesi

Son bir köprü, çünkü bu cümle hem doğru hem yanıltıcı. Senin Dart kodun tek thread'de — root isolate'in UI task runner'ında — koşar, doğru. Ama Flutter engine tek thread değildir. Engine kendi thread'lerini yaratmaz; bunu embedder (iOS'ta platform shell) yapar ve engine dört task runner ister:

Güncel bir gelişme de var: son sürümlerde (iOS/Android'de 3.29 ile başlayıp 3.32'de varsayılan olan bir roll-out'la) UI ve platform thread'leri birleştirildi — ayrı UI thread kaldırıldı, Dart kodu doğrudan native platform thread'inde koşuyor. Yani "Dart ayrı bir UI thread'inde çalışır" klasik anlatımı artık güncel değil; ironik biçimde bu, Flutter'ı iOS'un UIKit modeline (main thread'de çalışmak) daha da yaklaştırıyor.

Tek bakışta

BoyutDartSwift
ModelTek thread + event loopGerçek çok-thread'li havuz
Paralellik birimiIsolate (ayrı heap)Task + cooperative pool (paylaşımlı heap)
İletişimMessage passing (kopyala / taşı)Paylaşılan bellek + actor
Data race önlemiİzolasyon (paylaşım yok)Derleme-zamanı denetim (Sendable / actor)
await sonrası threadHep aynı (isolate'in thread'i)Garanti yok
UI kuralıUI runner / platform thread@MainActor = main thread
Varsayılan yönTek thread (baştan)Tek thread'e doğru (6.2 ile)

Zihinsel model

Tek cümle: Dart, data race'i paylaşmayı yasaklayarak eler (ayrı heap + mesajlaşma); Swift, paylaşımı derleyiciye ispatlatarak güvenli kılar (actor + Sendable). Biri "hiç dokunma", diğeri "dokun ama ispatla" der. Flutter'dan geliyorsan sezgin izolasyona kuruludur; iOS'ta o sezgiyi @MainActor ve actor'ın compile-time garantileriyle yeniden kur. Ve Swift 6.2'nin "varsayılan main actor" dünyasında, farkında olmadan o tanıdık tek-thread'li huzura geri dönmüş olacaksın.