İzole Et ya da Güvenle Paylaş — Dart ve Swift'in İki Eşzamanlılık Felsefesi
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:
- microtask queue — önceliklidir;
Futuretamamlanmaları veawaitdevamları buraya düşer. - event queue — timer, I/O, gesture ve isolate mesajları.
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.
- actor (SE-0306, Swift 5.5): mutable state'i sarar; ona aynı anda tek bir task erişir (mutual exclusion), üstelik thread'i bloklamadan. Senkronize edilmemiş erişim bir runtime disiplini değil, doğrudan derleme hatasıdır.
- Sendable: bir tipin izolasyon sınırlarını güvenle geçebileceğini işaretleyen protokol. Value type'lar (struct/enum) üyeleri Sendable ise Sendable olur. Actor sınırını geçen argüman ve sonuç Sendable olmak zorundadır; derleyici bunu statik denetler.
- @MainActor: main thread'i temsil eden actor. Runtime'da
DispatchQueue.mainile birebir yer değiştirilebilir.@MainActorişaretli kod yalnızca main thread'de koşar — iOS'un "UI sadece main thread'de" kuralının derleyici tarafından garanti edilmiş hâli.
@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:
- MainActor by default (SE-0466): modül bazında opt-in bir ayarla (
-default-isolation MainActor) işaretlenmemiş kod artık varsayılan olarak@MainActor'a izole olur. Kod varsayılan olarak tek thread'li başlar — paralelliği sen açıkça isteyene kadar. - @concurrent (SE-0461 ile gelen): "bunu gerçekten paralel koştur" demenin açık yolu. İşaretlenmemiş
nonisolated asyncfonksiyonlar ise artık varsayılan olarak çağıranın actor'ında kalır (önceden hep global havuza atlıyorlardı).
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:
- Platform — OS'un ana thread'i; engine ile her etkileşim burada olmak zorunda (iOS main-thread kuralının Flutter karşılığı).
- UI — tüm root isolate Dart kodu (build/layout/paint, timer, microtask) burada koşar.
- Raster — C++ engine'in sahneyi GPU için rasterize ettiği yer.
- IO — asset/görsel kod çözme gibi yardımcı işler.
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
| Boyut | Dart | Swift |
|---|---|---|
| Model | Tek thread + event loop | Gerçek çok-thread'li havuz |
| Paralellik birimi | Isolate (ayrı heap) | Task + cooperative pool (paylaşımlı heap) |
| İletişim | Message passing (kopyala / taşı) | Paylaşılan bellek + actor |
| Data race önlemi | İzolasyon (paylaşım yok) | Derleme-zamanı denetim (Sendable / actor) |
await sonrası thread | Hep aynı (isolate'in thread'i) | Garanti yok |
| UI kuralı | UI runner / platform thread | @MainActor = main thread |
| Varsayılan yön | Tek 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.