Zig’in Yeni Asenkron I/O’su
(kristoff.it)- Zig’in yeni asenkron I/O arayüzünün sunulmasıyla, I/O uygulama yönteminin çağıran tarafından doğrudan seçilip enjekte edilmesi mümkün hale geliyor
- Yeni tasarlanan Io arayüzü, aynı anda hem asenkronluğu hem paralelliği destekliyor ve kod yeniden kullanılabilirliği ile optimizasyona odaklanıyor
- Blocking I/O, event loop, thread pool, green thread, stackless coroutine gibi çeşitli standart kütüphane uygulamalarının sunulması planlanıyor
- Yeni API ile future iptali ve kaynak yönetimi, buffering ve daha ayrıntılı giriş/çıkış davranışları mümkün oluyor
- Mevcut function coloring sorununu çözerek, tek bir kütüphaneyle hem senkron hem asenkron çalışma optimize edilebilir hale geliyor
Genel Bakış
Zig, yakın zamanda yeni bir asenkron I/O arayüzü tasarlayarak, I/O işlemlerinde esneklik ve paralellik desteğine odaklanan bir yöne evriliyor. Bu değişiklik, mevcut async/await paradigmasını ayırarak, gerçek program yazarlarının çok daha çeşitli I/O stratejilerini benimseyebilmesine olanak tanıyacak şekilde tasarlandı.
Yeni I/O Arayüzü
Önceden I/O ile ilgili nesneler doğrudan kod içinde oluşturulup kullanılıyordu; artık ise Io arayüzü çağıran tarafından enjekte ediliyor.
- Bu yöntem, Allocator desenine benzer şekilde, çağıran tarafın somut I/O uygulamasını seçip enjekte etmesini sağlıyor
- Harici paket kodlarında da I/O stratejisi tutarlı bir şekilde uygulanabiliyor
Başlıca değişiklikler
- Io arayüzü artık eşzamanlılık (concurrency) işlemlerini de üstleniyor
- Kod eşzamanlılığı doğru ifade ettiğinde, Io uygulamasına bağlı olarak paralellik (parallelism) sağlanabiliyor
Örnek kod
- Eşzamanlılığı olmayan (seri) kod ile
io.asyncveawaitüzerinden paralellik olasılığını ifade eden kod karşılaştırılıyor- Seri kod: iki dosyaya sırayla kaydeder, paralellik fırsatını kullanamaz
- Paralel kod: future’lardan yararlanarak dosya kaydeder, asenkron event loop üzerinde daha verimli çalışır
await ve try birleşimi
awaitiletrybirlikte kullanıldığında, bir future’da hata oluşursa diğer future’ın kaynaklarını iade edememe sorunu vardırdefervefuture.cancelile uygun iptal ve temizleme açık biçimde yapılabilir
Future.cancel API’si
Future.cancel()veFuture.await()idempotenttir (birden çok kez çağrılsa da yan etki oluşturmaz)- Tamamlanmış bir future üzerinde
cancelçağrılırsa yalnızca kaynaklar serbest bırakılır; tamamlanmamış işlererror.Canceleddöndürür
Standart Kütüphane I/O Uygulamaları
Io arayüzü, çalışma zamanında çok biçimliliğe dayalı bir arayüzdür; doğrudan uygulanabilir veya üçüncü taraf paketlerin uygulamaları kullanılabilir. Zig’in standart kütüphanesi, çeşitli I/O uygulama türleri sunmayı planlıyor.
- Blocking I/O: Mevcut C tarzı blocking giriş/çıkışın doğrudan kullanılması, ek yük yok
- Thread pool: Blocking I/O işlemlerinin OS thread pool’una dağıtılması, bir miktar paralellik sağlar. Ağ istemcileri gibi alanlarda optimizasyon gerekebilir
- Green thread: Linux’un
io_uringgibi asenkron sistem çağrılarından yararlanarak, bir OS thread üzerinde birden çok green (hafif) thread’i işler. Platform desteği gerekir (x86_64 Linuxöncelikli) - Stackless coroutine: Açık bir stack gerektirmeyen, durum makinesi tabanlı coroutine’ler. WASM gibi bazı platformlarla uyumluluk amacı taşır. Zig derleyicisinde ilgili convention’ın yeniden eklenmesi gerekir
Tasarım Hedefleri
Kod Yeniden Kullanılabilirliği
Asenkron I/O’daki en büyük sorun kodun yeniden kullanılabilirliğidir; diğer dillerde blocking/async fonksiyonlar ayrı bulunduğundan kodun bölünmesi gibi bir problem vardır. Zig’in yaklaşımı ise şunları sağlar:
- Tek bir kütüphane hem senkron hem asenkron modu etkili biçimde destekleyebilir
- async/await, “function coloring” olgusunu ortadan kaldırır ve Io sistemi sayesinde çalışma zamanında da çeşitli yürütme modellerine bağımlı kalmaz
Sonuç olarak function coloring sorunu tamamen çözülüyor
Optimizasyon
- Yeni Io arayüzü, generic olmayan ve vtable tabanlı sanal çağrı yöntemiyle uygulanıyor
- Sanal çağrılar kod şişmesini azaltır, ancak çalışma sırasında az miktarda ek yük oluşturur. Optimize derlemelerde tek bir Io uygulaması varsa de-virtualization (sanal çağrının kaldırılması) mümkündür
- Birden fazla Io uygulaması kullanıldığında sanal çağrı korunur (kod tekrarını önlemek amacıyla)
Buffering stratejisi
- Önceden buffering her uygulama (
reader/writer) tarafından üstleniliyordu; artık buffering, Reader ve Writer arayüzü seviyesinde yapılıyor - Buffer flush dışında sanal çağrı yoluna girilmediği için optimizasyon kolaylaşıyor
Semantik I/O İşlemleri
Writer arayüzü, belirli optimizasyon işlemleri için iki yeni primitive sunuyor
- sendFile: POSIX
sendfile’dan ilham alır; dosya tanımlayıcıları arasında veri aktarımını çekirdek içinde gerçekleştirir. Bellek kopyalamayı en aza indirir - drain: Vectorized write + splatting desteği sağlar. Birden fazla veri segmentini topluca gönderir ve
writevsistem çağrısına dönüştürülebilir.splatparametresiyle son öğenin tekrar tekrar kullanılması mümkündür (sıkıştırma gibi akışlarda kullanılabilir)
Yol Haritası
Bu değişikliğin bir bölümü Zig 0.15.0’dan itibaren sunulacak, ancak kütüphanede büyük ölçekli yeniden düzenleme gerektiği için tam geçişin bir sonraki sürümü beklemesi gerekiyor. SSL/TLS, HTTP server/client gibi başlıca modüllerin de yeni Io sistemiyle yeniden tasarlanması planlanıyor
SSS
S: Zig düşük seviyeli bir dilken neden async önemli?
- Zig, sağlamlık, optimizasyon ve yeniden kullanılabilirlik hedefler
- Non-blocking giriş/çıkışı standartlaştırarak, diğer kütüphanelerin ve üçüncü taraf kodların da genel I/O stratejisine uyum sağlaması ve yeniden kullanılabilirliğin korunması amaçlanır
S: Paket yazarlarının artık tüm kodlarında async kullanması mı gerekiyor?
- Hayır. Tüm kodun eşzamanlılığı ifade etmesi gerekmez
- Genel amaçlı sıralı kod da kullanıcının seçtiği I/O stratejisine uygun şekilde çalışır
S: Hangi yürütme modeli olursa olsun, sadece eklenti takınca her şey mutlaka düzgün çalışır mı?
- Çoğunlukla evet
- Ancak koddaki programlama hataları (ör. eşzamanlı iş gereksinimlerinin karşılanmaması) varsa düzgün çalışmaz
Çalışma örnekleriyle birlikte, asenkronluk ve paralellik arasındaki fark ile doğru çalışma akışını tasarlama gerekliliğine de değiniliyor
Sonuç
Zig, yeni Io arayüzünü tanıtarak giriş/çıkış stratejisi seçiminde esnekliği, kodun yeniden kullanılabilirliğini ve optimizasyon imkânını büyük ölçüde artırıyor. Böylece geliştiriciler, asenkron/senkron temelli fonksiyon yazım kısıtları olmadan eşzamanlılık ve paralellik yapısını daha açık ifade edebilir, ayrıca farklı platformlar ve yürütme modellerine de etkili biçimde uyum sağlayabilir.
1 yorum
Hacker News görüşleri
Bu noktayı yeniden vurgulamak istiyorum. Yazıda Zig'in function coloring sorununu tamamen çözdüğü bile söyleniyor ama ben buna katılmıyorum. Meşhur "What color is your function?" yazısındaki 5 kuralı yeniden düşünürsek, Zig'de async/sync/red/blue gibi renkler ayrılmasa da sonuçta yine yalnızca iki durum var: IO fonksiyonları ve IO olmayan fonksiyonlar. Fonksiyon çağırma biçiminin renge göre değişmesi sorununu teknik olarak çözmüş olabilirler, ama hâlâ IO gereken fonksiyonlara IO'yu argüman olarak vermeniz gerekiyor, gerekmeyenler ise almıyor. Sonuçta özün değişmediği hissi var. IO fonksiyonları yine yalnızca IO fonksiyonlarından çağrılabiliyor ve bu da coloring sorunundan çıkamadığı anlamına geliyor. Elbette yeni bir executor da geçirilebilir ama bunun gerçekten istenen şey olup olmadığı şüpheli. Rust'ta da benzer bir şey yapılabilir. Renkli fonksiyon çağrılarının zahmetli olması da aynı şekilde geçerli. Bazı çekirdek kütüphane fonksiyonlarının colored olması meselesi ise Zig/Rust için de aynı değil. Coloring sorununun özü, context'e yani async executor, auth, allocator vb. gerektiren fonksiyonların çağrılırken bu context'in mutlaka sağlanması gerekliliğidir. Zig'in bunu gerçekten çözdüğünü söylemek zor. Yine de Zig'in soyutlaması çok iyi yapılmış ve Rust bu konuda biraz daha zayıf kalıyor. Ama function coloring sorununun kendisi hâlâ duruyor
Klasik async function coloring'e kıyasla temel fark şu: Zig'deki 'Io', yalnızca asenkron işlem için kullanılan özel bir değer değil; dosya okuma, sleep, zamanı alma gibi tüm IO için zorunlu olarak gereken bir değer. 'Io' bir fonksiyonun özelliği değil, her yerde bulunabilen sıradan bir değer. Pratikte bu özellik sayesinde coloring sorunu çözülmüş gibi görünüyor. Çoğu kod tabanında IO zaten kapsamın bir yerinde bulunduğu için, yalnızca gerçekten saf hesaplama yapan fonksiyonların IO'ya ihtiyacı kalmıyor. Bir fonksiyon aniden IO'ya ihtiyaç duymaya başlarsa, çoğu durumda bunu doğrudan 'my_thing.io' içinden alıp kullanabiliyorsunuz. Rust'taki gibi her fonksiyona Allocator geçmek zorunda kalmadığınız için uğraştırmıyor. Yani kod yolu değişip IO yapmanız gerekirse, değişikliği tek tek fonksiyonlara yaymanız gerekmeden doğrudan kullanabiliyorsunuz. Teorik olarak function coloring'in hâlâ var olduğuna katılıyorum, ama fiilen neredeyse tüm fonksiyonlar async-colored olmuş oluyor; bu yüzden pratikte sorun neredeyse yok. Nitekim Zig geliştiricileri de Allocator'ı açıkça geçirmenin function coloring kaynaklı bir zahmet oluşturmadığını düşünüyor. 'Io' için de benzer şekilde büyük bir sorun olmayacağını düşünüyorum
Bence önemli ana nokta atlanmış. Rust kütüphanelerini kullanırken async/await, tokio, send+sync gibi koşulları mutlaka sağlamak gerekiyor ve API sync ise async uygulamada fiilen işe yaramaz hâle geliyor. Buna karşılık Zig'in IO geçirme yaklaşımı bu sorunu temelden çözüyor. Böylece uğraştırıcı procedural macro'lara veya yapay çoklu sürümlemelere gerek kalmıyor; zaten bu tür yaklaşımlar da kütüphane çoklu sürüm sorununu gerçekten iyi çözmüyor. Rust'ta async/sync karışımıyla ilgili çeşitli tartışmalar var; şu bağlantıda da anlatılıyor: https://nullderef.com/blog/rust-async-sync/. Umarım Zig ileride cooperative scheduling, yüksek performanslı async ve thread-per-core async gibi konuları da iyi çözer
Kategori kuramı uzmanı değilim ama sonunda bu tür context yönetimi yoluna girince varılan yer IO monad oluyor. Bu bağlam Context olarak örtük olabilir ama derleyici yardımından düzgün faydalanmak istiyorsanız sistem içinde somut bir varlık olarak ortaya çıkması gerekiyor. Sistem programlama dillerinin büyük hedefleri hep Async ya da coroutine mezarlığında gömüldü, ama Andrew'nun IO monad'ı bir bakıma yeniden keşfedip düzgün uygulaması bu kuşak için umut verici. Gerçek dünya fonksiyonlarının renkleri vardır. Ya net geçiş kuralları koyarsınız ya da C++'taki co_await ve tokio gibi giderek karmaşıklaşan yola saparsınız. Bana göre bu tam olarak ‘The Way’
Tüm fonksiyonları kırmızıya ya da maviye boyamanın basit bir hilesi var
io'yu global değişken yapıp kullanırsanız coloring diye dert kalmaz. Şaka bir yana, elbette 'Io' arayüzünü kullanma zorunluluğu biraz sürtünme yaratıyor ama bu, async/await kullanırken ortaya çıkan gerçek friction ile özünde farklı bir mesele. Bana göre function coloring sorununun özü, async anahtar sözcüğünün statik renk ataması yüzünden kod yeniden kullanımını imkânsızlaştırması. Zig'de bir fonksiyonu async yapmak ya da yapmamak fark etmiyor, her durumda IO argümanını alıyor; bu açıdan coloring'in kendisi anlamsızlaşıyor. İkinci olarak, async/await kullanınca stackless coroutine yani derleyici kontrollü stack switching zorunlu oluyor; ama Zig'in yeni IO sistemi içeride async kullansa bile Blocking IO olarak çalışabilecek şekilde düzenlenebiliyor. Bana göre asıl pratik function coloring sorunu bu
Go da “ince bir coloring” sorunu yaşıyor. Goroutine kullanırken iptal işlemleri için her zaman context argümanı geçirmek gerekiyor ve birçok kütüphane fonksiyonu da context istiyor; bu da tüm kod tabanına yayılıyor. Teknik olarak context kullanmamak mümkün ama rastgele context.Background geçirmek önerilen bir yaklaşım değil
sans-io kavramı Rust vb. dillerde daha önce de tartışıldı; ilgili bağlantılar: https://www.firezone.dev/blog/sans-io, https://sans-io.readthedocs.io/, https://news.ycombinator.com/item?id=40872020
Bence function coloring'in sorunu, stack üzerinde çözseniz de stack'i unwind etseniz de sonunda iki seçenekten birinin kalması. Zig coloring sorununu çözdüğünü iddia ediyor ama IO implementasyonu olarak hâlâ blocking/thread pool/green thread kullanabilmeyi mümkün kılıyor. Oysa bu tür blocking IO zaten baştan sorun olan şey değildi. Global state kullanmama geleneğine uyduğunuz sürece bu düzeyde bir şeyi neredeyse her dilde yapabilirsiniz. Stackless coroutine henüz uygulanmamış durumda; biraz “kalan parçaları da çizersen tamam” hissi veriyor. Eğer gerçekten evrensel fonksiyon çağrısı istiyorsak, bence iki yol var
Tüm fonksiyonları async yapmak ve bir argümanla senkron çalışıp çalışmayacağını belirlemek (performans kaybı olur)
Her fonksiyonu iki kez derleyip duruma göre uygun olanı çağırmak (kod boyutu artar ve function pointer yönetimi zorlaşır)
Çekirdek ekipten değilim ama duyduğum kadarıyla kullanıcılar ve gerçek kullanıcılar semiblocking implementasyonları yeterince deneyip API'yi oturttuktan sonra, tam da o çözümü yani stack jumping tabanlı gerçek coroutine eklemeyi planlıyorlar. Şu anda LLVM'in coroutine state machine derleyicisinin libc ya da malloc bağımlılığı gibi sorunları var. Zig'in yeni io arayüzü userland async/await'i desteklediği için, ileride düzgün bir frame jumping çözümü geldiğinde taşımak kolay olacak ve debug etmek de rahatlayacak. Coroutine işi zor çıkarsa, io API'si de küçük düzeltmelerle dayanabilecek şekilde tutuluyor; yani stackless coroutine'e fazla acele etmiyorlar
C#/.NET'teki ValueTask<T> de benzer bir rol oynuyor. İş senkron tamamlanırsa ek yük olmuyor, gerekirse yalnızca o zaman Task<T> olarak kullanılabiliyor. Kod tarafında genelde sadece await yazıyorsunuz; çalışma anında runtime ya da derleyici senkron/asenkron seçimini kendisi yapıyor
Zig'i seviyorum ama green thread'e yani fiber/stackful coroutine tarafına odaklanmaları biraz hayal kırıklığı yaratıyor. Rust da 1.0 öncesinde benzer bir Runtime trait'i performans sorunları nedeniyle kaldırmıştı. Aslında OS, dil ve kütüphane ekosistemleri bu yaklaşımın zararlarını defalarca gördü ve bununla ilgili kaynaklar da var: https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1364r0.pdf. Fiber'lar 90'larda ölçeklenebilir eşzamanlılık için popülerdi ama bugün stackless coroutine'ler, OS/donanım ilerlemeleri vb. nedenlerle artık önerilmiyor. Eğer böyle devam ederse Zig, Go benzeri bir performans sınırına çarpabilir ve gerçek bir performans rakibi olmakta zorlanabilir. std.fs'nin performans gereken senaryolarda elde kalmasını umuyorum
Green thread'e yani fiber'lara “tamamen yüklendiğimiz” izlenimi yanlış. OP'nin bağlantı verdiği yazıda stackless coroutine tabanlı bir implementasyon beklendiği açıkça belirtiliyor ve buna dair öneri de var: https://github.com/ziglang/zig/issues/23446. Performans önemli ve fiber'lar performans açısından beklentiyi karşılamazsa yaygın çözüm olmayacaktır. Bu yazıda tartışılanlar, stackless coroutine'in varsayılan 'Io' implementasyonu olmasına engel değil
Green thread'in performansının kötü olduğu iddiasından emin değilim. Başlıca eşzamanlı sunucu platformlarının hepsi Go, Erlang, Java green thread kullanıyor ya da kullanmaya çalışıyor. Green thread'ler C FFI ile uyumluluk sorunları nedeniyle Rust gibi daha düşük seviyeli diller için uygun olmayabilir ama performansın kendisinin her zaman sorun olduğunu söylemek zor
Bu, birçok seçenekten yalnızca biri olduğu için buna ‘all-in’ demek doğru değil. Hangi implementasyonun seçileceğine executable karar verir; kütüphane kodu değil
Zig de Rust'ın green thread'i kaldırıp async runtime'a geçmesindekine benzer bir etkiyi hedefliyor. Buradaki temel sezgi, ‘async=IO, IO=async’ fikrini resmileştirmesi. Rust tokio gibi pluggable async runtime'lar sunarken, Zig pluggable IO runtime sunuyor. Sonuçta yönelim, runtime'ı dilden çıkarıp kullanıcı alanında takılabilir hâle getirmek ve herkesin ortak bir arayüz paylaşmasını sağlamak
Kaynak (P1364R0) tartışmalıydı ve bana göre belirli bir yaklaşımı ortadan kaldırmak amacıyla motive edilmiş iddialar içeriyordu. Karşı argümanlar için şu bağlantılara da bakılabilir: https://old.reddit.com/r/cpp/comments/1jwlur9/stackful_coroutines_faster_than_stackless/, https://old.reddit.com/r/programming/comments/dgfxde/fibers_arent_useful_for_much_any_more/f3bmpww/
Zig gibi bir sistem dilinde, yaygın standart IO işlemlerinde bile runtime polymorphism'i zorunlu kılmak biraz garip geliyor. Çoğu gerçek kullanımda IO implementasyonu statik olarak belirlenebilirken neden runtime overhead dayatılsın ki?
IO'da dynamic dispatch overhead'inin pratikte neredeyse önemsiz kalacağını düşünüyorum. IO hedefi ne olduğuna göre değişir elbette ama sonuçta CPU darboğazı olmayan IO vakaları çok daha yaygın. Zaten buna IO-bound deniyor
“Neden herkese runtime overhead dayatılıyor?” sorusuna karşılık, çoğu durumda tek tür io kullanan sistemlerde derleyicinin double indirection maliyetini optimize edip ortadan kaldırmasının amaçlandığı anlaşılıyor. Ayrıca IO'da zaten darboğaz başka yerde olduğu için bir seviye daha indirection eklenmesi pek yük oluşturmaz
Zig'in felsefesinde binary size daha fazla önemseniyor. Allocator'da da aynı trade-off var; örneğin ArrayListUnmanaged allocator için generic değil, bu yüzden her allocation'da dynamic dispatch oluşuyor. Pratikte dosya allocation'ı ya da write maliyeti, dolaylı çağrı overhead'ini fazlasıyla bastırıyor. Binary boyutuna bu kadar odaklanmak Zig tarzı. Bu arada devirtualization yani dinamik çağrıyı statik çağrıya çeviren optimizasyon biraz şehir efsanesi
Runtime polymorphism özünde kötü bir şey değil. Tight loop içinde branch üretmesi ya da derleyicinin inline optimizasyonu yapamaması gibi durumlar yoksa, sorun sayılmaz
Yeni io parametresinin her yerde görünmesi çok hoşuma gitmiyor ama farklı implementasyonları thread tabanlı, fiber tabanlı vb. kolayca kullanabilme ve kullanıcıya bir implementasyon dayatmama fikri Allocator arayüzündeki gibi çok hoşuma gidiyor. Genel olarak ciddi bir iyileştirme ve çeşitli stdlib implementasyonları arasında ek overhead olmadan sync/blocking io implementasyonu da sunulursa, Zig'in “kullanmadığın şeyin bedelini ödemezsin” felsefesine tam uymuş olur
Zig'de io.async, asenkronluğu yani işlemlerin sırasının garanti edilmeyebileceğini ama sonucun doğru olacağını ifade ediyor; concurrency'yi ifade etmiyor. Yani async ile io çağrılarının anlamını ayırmış olmaları asıl önemli nokta. Bence bu tasarım çok zekice
IO arayüzü sayesinde dil seviyesinde bir vfs Virtual File System oluşturulabilmesi hoşuma gidiyor
Zig öğrenmek için basit bir ssh sunucusu yazmayı denedim. Bu yeni IO/event loop yapısı sayesinde kod akışını çok daha kolay anlayabildim. Andy'ye teşekkürler
Yazı çok iyi yazılmış, okumak çok ilginçti. Özellikle WebAssembly tarafındaki çıkarımları heyecan verici buldum. WASI'yi userspace'te kullanabilmek ve Bring Your Own IO yaklaşımının mümkün olması gerçekten çok ilginç