Async Rust hiçbir zaman MVP durumunu aşamadı
(tweedegolf.nl)- Async Rust, executor’dan bağımsız kodun sunucularda ve mikrodenetleyicilerde birlikte çalışmasını mümkün kılıyor; ancak derleyicinin ürettiği durum makinesi nedeniyle özellikle gömülü tarafta ikili dosya boyutundaki artış belirgin oluyor
bar()gibi iki adet await noktası olan basit bir örnek bile 360 satırlık MIR ileUnresumed,Returned,Panicked,Suspend0,Suspend1durumlarını üretiyor; eşzamanlı sürüm ise yalnızca 23 satır gerektiriyor- Tamamlanmış bir future yeniden poll edildiğinde
panicyerinePoll::Pendingdöndürecek şekilde değiştirmek, unsafe davranış olmadan sözleşmeyi karşılamayı mümkün kılıyor ve deneylerde gömülü firmware’in ikili dosya boyutunu %2 ila %5 azaltıyor - Await içermeyen
async { 5 }bile şu anda varsayılan üç durumlu bir durum makinesi üretiyor; ancak her seferindePoll::Ready(5)döndürecek şekilde optimize edilirse gömülü ikili boyutu %0,2 azalıyor - Önerilen Project Goal, sürüm modunda tamamlanma sonrası panic’in kaldırılması, await içermeyen async block’larda durum makinesinin kaldırılması, tek await’li future’ın inline edilmesi ve aynı durumların derleyicide birleştirilmesini hedefliyor
Async Rust’ta derleyici seviyesinde şişme sorunu
- Async Rust, executor’dan bağımsız kodun sunucularda ve mikrodenetleyicilerde aynı anda çalışmasını sağlıyor; ancak küçük mikrodenetleyicilerde ikili dosya boyutundaki artış özellikle dikkat çekiyor
- Rust blogu async/await’i sıfır maliyetli soyutlama olarak tanıttı; ancak async gerçekte ciddi miktarda şişme yaratıyor ve masaüstü ile sunucu tarafında da aynı sorun var, sadece bellek ve işlem kaynakları fazla olduğu için daha az görünür kalıyor
- Async kod yazarken şişmeyi önlemeye yönelik geçici çözümlerin ardından, sorunu derleyici tarafında çözmek için bir Project Goal sunuldu
- Future’ın gerekenden fazla büyümesi ve çok sayıda kopyalama yapılması kapsam dışında bırakıldı
- Bu sorun zaten biliniyor ve bir kısmını ele alan bir PR açık durumda: https://github.com/rust-lang/rust/pull/135527
Üretilen future’ın yapısı
- Örnek kodda
foo()async { 5 }döndürüyor vebar()isefoo().await + foo().awaitçalıştırıyor- Godbolt örneği: godbolt
bariçinde iki await noktası bulunduğundan durum makinesinde en az iki durum gerekse de, gerçekte daha fazla durum üretiliyor- Rust derleyicisi çeşitli geçişlerde MIR dökümü yapabiliyor ve
coroutine_resumegeçişi son async’e özgü MIR geçişi- Async, LLVM IR’de artık yer almasa da MIR’de kalıyor; dolayısıyla async’in durum makinesine dönüştürülme süreci MIR geçişlerinde gerçekleşiyor
barfonksiyonu 360 satırlık MIR üretiyor; eşzamanlı sürüm ise yalnızca 23 satır kullanıyor- Derleyicinin çıktıladığı
CoroutineLayout, fiilen enum biçimindeki bir durum kümesiUnresumed: başlangıç durumuReturned: tamamlanmış durumPanicked: panic sonrası durumSuspend0: ilk await noktası vefoofuture’ını saklıyorSuspend1: ikinci await noktası ve ilk sonucu ile ikincifoofuture’ını saklıyor
Future::pollgüvenli bir fonksiyon olduğundan, future zaten tamamlandıktan sonra tekrar çağrılsa bile UB’ye yol açmamalı- Şu anda
Suspend1sonrasındaReadydöndürüp future’ıReturneddurumuna geçiriyor - Bu durumda yeniden poll edilirse panic oluşuyor
- Şu anda
Panickeddurumu, async fonksiyon panic verdikten sonra bu paniccatch_unwindile yakalandığında ilgili future’ın tekrar poll edilmesini engellemek için var gibi görünüyor- Panic sonrasında future eksik bir durumda kalabilir; bu yüzden yeniden poll etmek UB’ye yol açabilir
- Bu mekanizma mutex poisoning’e oldukça benziyor
Panickeddurumuna ilişkin bu yorum için kesin bir belge bulmak zor; bu yüzden buna dair güven düzeyi yaklaşık %90
Tamamlandıktan sonra poll edildiğinde gerçekten panic gerekli mi?
Returneddurumundaki future şu anda panic veriyor, ancak bunun zorunlu olması gerekmiyor- Gerekli olan tek koşul, UB’ye yol açmaması
- Panic görece maliyetli ve optimizasyonla kaldırılması zor olan yan etkili bir yol ekliyor
- Tamamlanmış future yeniden poll edildiğinde
Poll::Pendingdöndürmek, unsafe davranış olmadanFuturetürünün sözleşmesini karşılayabiliyor - Derleyici bu yaklaşımla değiştirilip deney yapıldığında, async gömülü firmware’de ikili dosya boyutunda %2 ila %5 azalma görüldü
- Bu davranışın, tıpkı tamsayı taşmasındaki
overflow-checks = falsegibi bir anahtarla sunulması öneriliyor- Hatalı davranışı hemen görünür kılmak için debug derlemelerinde panic devam ediyor
- Release derlemelerinde ise daha küçük future’lar elde edilebiliyor
panic=abortkullanıldığındaPanickeddurumunun kendisinin tamamen kaldırılması mümkün olabilir; bunun etkisi ayrıca değerlendirilmeli
Await olmasa bile her zaman durum makinesi üretiliyor
foo()yalnızcaasync { 5 }döndürdüğünden, elle yazılmış en iyi uygulama durum içermeyen ve her zamanPoll::Ready(5)döndüren bir future olurdu- Ancak derleyicinin ürettiği MIR’de yine de
Unresumed,Returned,Panickedolmak üzere temel üç durum bulunuyor- Poll sırasında mevcut durumun discriminant’ı kontrol edilip dallanılıyor
- Tamamlandıktan sonra yeniden poll edilirse
`async fn` resumed after completionassert’i ile panic veriliyor
- Bu durumda durum makinesi üretmek yerine her seferinde
Poll::Ready(5)döndürecek şekilde optimize etmek mümkün - Bu değişiklik derleyiciye deneysel olarak uygulandığında gömülü ikili boyutu %0,2 azaldı
- Kazanç büyük değil, ancak basit bir optimizasyon olduğu için uygulanmaya değer olabilir
- Bu optimizasyon davranışı bir miktar değiştiriyor, ancak etkilenecek olanlar yalnızca sözleşmeye uymayan executor’lar
- Mevcut derleyici sonraki poll işleminde panic veriyor
- Optimizasyondan sonra ise future her zaman
Readydöndürüyor
Yalnızca LLVM yeterli değil
- MIR çıktısı verimsiz olsa bile LLVM’nin her şeyi temizleyebildiği durumlar var, ancak koşullar sınırlı
- Future yeterince basit olmalı
opt-level=3kullanılmalı
- Future karmaşıklaştıkça LLVM bunu kaldıramıyor; deyimsel async Rust kodunda future’lar derin biçimde iç içe geçtiği için karmaşıklık hızla büyüyor
- Gömülü sistemler veya wasm gibi boyut optimizasyonunun sık yapıldığı ortamlarda LLVM her şeyi optimize edemiyor
- Godbolt örneği: https://godbolt.org/z/58ahb3nne
- Üretilen assembly’de LLVM,
foo’nun 5 döndürdüğünü biliyor amabarsonucunu 10’a optimize edemiyor fooiçinpollfonksiyonu çağrısı da hâlâ duruyor- Bunun nedeni, derleyicinin tamamen çözemediği potansiyel panic yolları
- LLVM,
foo’nun pratikte yalnızca bir kez çağrıldığını ve panic vermediğini bilmiyor
- Üretilen assembly’de LLVM,
- IR içindeki panic dalları yorum satırına alındığında optimizasyon daha iyi oluyor: https://godbolt.org/z/38KqjsY8E
- LLVM’den sonradan optimizasyon beklemek yerine, derleyicinin LLVM’ye daha iyi girdi vermesi gerekiyor
Future inline etme iyi çalışmıyor
- Inline etme, sonrasındaki optimizasyon geçişlerini mümkün kıldığı için önemli; ancak üretilen Rust future’ları şu anda erken aşamada inline edilmiyor
- Her future kendi implementasyonunu aldıktan sonra LLVM ve linker inline etme fırsatı yakalıyor, ancak önceki sorunlar nedeniyle bu aşama artık çok geç kalıyor
- En doğrudan inline fırsatı,
bar()fonksiyonunun yalnızcafoo(blah).awaityapması durumu- Trait kullanılarak soyutlama kurulurken sık görülen bir desen
- Mevcut derleyici
bariçin bir durum makinesi oluşturuyor ve onun içindefoodurum makinesini çağırıyor - Daha verimli yaklaşımda
bar, doğrudanfoofuture’ı olabilir
- Preamble ve postamble olduğunda durum daha karmaşık
- Örneğin
bar(input),input > 10ileblaholuşturuyor, ardındanfoo(blah).awaityapıyor ve sonuca* 2uyguluyor - Bu, özellikle trait implementasyonlarında async fonksiyonları farklı imzalara dönüştürürken sık görülüyor
- Örneğin
- Bu tür bir
barda kendi async durumuna ihtiyaç duymuyor- Tek await noktasını aşarak korunması gereken veri,
fooiçinde yakalanan değerler dışında yok - Yine de
bardoğrudanfoo’nun kendisi olamaz; fakat durumun büyük bölümüfooya bırakılabilir
- Tek await noktasını aşarak korunması gereken veri,
- Elle yazılmış bir uygulamada
BarFut,Unresumed { input }veInlined { foo: FooFut }durumlarına sahip olabilir- İlk poll’da preamble çalıştırılır,
foo(blah)oluşturulur veInlineddurumuna geçilir - Sonrasında
foo.poll(cx)sonucuna postamble uygulanır
- İlk poll’da preamble çalıştırılır,
- İlk await noktasından önce kodu önceden çalıştırmak mümkün olsaydı
Unresumeddurumu da kaldırılabilirdi; ancak future’ın poll edilmeden önce hiçbir şey yapmaması garanti edildiği için bu değiştirilemez - Poll edilmekte olan bir future’ın özellikleri sorgulanabilse ek inline optimizasyonları mümkün olabilir
- Örneğin future’ın ilk poll’da her zaman ready döndürdüğü bilinseydi, çağıran future içinde o await noktası için durum oluşturmaya gerek kalmazdı
- Bu tür optimizasyonlar özyinelemeli biçimde uygulanırsa birçok future çok daha basit durum makinelerine indirgenebilir
- Mevcut
rustcyapısında her async block ayrı ayrı dönüştürülüyor ve sonrasında ilgili veriler korunmadığı için bu tür sorgular mümkün görünmüyor - Future inline etme henüz deneysel olarak uygulanmadı, ancak ikili boyut ve performans açısından büyük fayda sağlaması bekleniyor
Aynı durumların birleştirilmesi
- Async block içindeki her await noktası, durum makinesine ek bir durum ekliyor
- Aşağıdaki gibi bir kod doğal görünse de iki dalda da aynı async fonksiyon await edildiği için iki özdeş durum oluşuyor
CommandId::A => send_response(123).awaitCommandId::B => send_response(456).await
- Bu durumda
CoroutineLayoutiçindesend_responseiçin aynı coroutine türünü tutan_s0,_s1alanları ayrı ayrı oluşuyor veSuspend0,Suspend1adlı iki durum yaratılıyor - Bu fonksiyonun MIR’i 456 satır uzunluğunda ve birçok temel blok fiilen yinelenmiş durumda
- Kod önce yalnızca yanıt değerini hesaplayıp sonra tek kez
send_response(response).awaityapacak şekilde elle yeniden düzenlenirse yinelenen durumlar ortadan kalkıyorCommandId::Aiçin123CommandId::Biçin456- Sonrasında
send_response(response).await
- Yeniden düzenleme sonrasında
CoroutineLayoutiçinde depolanan tek bir future kalıyor ve yalnızca birSuspend0durumu bulunuyor - Toplam MIR uzunluğu 302 satıra düşüyor ve tekrar ortadan kalkıyor
- Bu nedenle aynı kod yollarını ve durumları bulup tekilleştiren bir optimizasyon geçişi faydalı görünüyor
- Bu optimizasyonun future inline etme geçişiyle iyi birleşmesi muhtemel
Deney bağlantıları ve ek benchmark’lar
- İki deney birlikte uygulandığında,
smolexecutor kullanan x86 sentetik benchmark’ta yaklaşık %3 performans artışı görülüyor - No panics in poll after ready: https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending
- No await, no statemachine: https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await
Project Goal için destek çağrısı
- Bu çalışma, derleyici tarafında ilerletilmek üzere bir Project Goal olarak sunuldu: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
- Finansman olmadan çok fazla ilerleme kaydetmek zor olduğu için, bu çalışmadan fayda sağlayacak şirket veya kuruluşların kısmi ya da tam desteğine ihtiyaç var
- İletişim adresi
dion@tweedegolf.com - İş kapsamı ve gerekli finansman miktarı esnek, ancak €30k ile işin tamamının ya da önemli bir bölümünün bitirilebileceği tahmin ediliyor
2 yorum
Hacker News görüşleri
Başlığın biraz abartılı olduğuna katılıyorum, ama metin iyi yazılmış ve ana fikir de iyi aktarılmış
Rust async konusunda güçlü bir görüş belirtecek kadar çok deneyimim yok ama birkaç şey gözüme çarptı
İyi tarafı, açık bir runtime kullanabilmeniz. Projenin tamamını async ile kirletmek yerine, varsayılanı senkron tutup runtime’ı yalnızca G/Ç “sınırlarında” kullanabiliyorsunuz
Üzerinde çalıştığım projeye bu yaklaşım iyi uydu ve Zig’in G/Ç kodunda izlediği stratejiye de epey benziyor gibi görünüyor. Bu durumda function coloring sorunu da büyük ölçüde çözülmüştü; ayrıca G/Ç ile CPU ağırlıklı kodu sıkı biçimde ayırmamız gerektiği için açık G/Ç runtime’ı doğal geldi
Kötü tarafıysa tüm ekosistemin tokio’ya fazla bağımlı görünmesi. Bu, Java’da GC’nin teoride isteğe bağlı olup pratikte herkesin aynı üçüncü taraf GC runtime’ını kullanmasına ve hangi kütüphaneyi alırsanız alın o runtime’ın dayatılmasına benziyor. Böyle merkezi bir bağımlılık sağlıklı değil
İş istasyonu işlemcilerindeki async runtime gereksinimleri ile RP2040 gibi ortamlardaki gereksinimler çok farklı. Buna rağmen backend değiştirilebildiği için, küçük ARM M0 mikrodenetleyiciler için async G/Ç kodu yazarken embedded odaklı embassy runtime’ını kullanırsanız kod başka ortamlarda yazdığınızla neredeyse aynı görünüyor
Aynı trait’leri ve arayüzleri kullandığınız için runtime ayrıntıları hakkında daha az düşünmeniz gerekiyor. Küçük bir RTOS kullanmakla ya da async ortamını kendiniz kurmakla kıyaslayınca oldukça iyi
embassy’de async kod yazarak öğrendiklerinizi başka alanlara da taşıyabiliyorsunuz
tokio standart kütüphanenin parçası olmasa da iyi bakılıyor, bu yüzden mevcut durum bana makul görünüyor. Hatta standart kütüphaneye girerse başka yürütücüler kullanmak zorlaşır ve standart kütüphaneyi başka platformlara taşımak da daha güç hale gelir diye endişeleniyorum
Tabii bu kaygı temelsiz de olabilir
Loglama bugün büyük ölçüde slf4j etrafında toparlandı ama hâlâ başka şeyler kullanan kütüphaneler var; ortak yardımcı araçlar önce Apache Commons’tı, şimdi ise çoğu yerde Guava kullanılıyor
JSON tarafı kısmen Jackson’da birleşti ama Gson ve Simple-json da yaygın; null izinliliği anotasyonları ise resmileşmeyen JSR-305’in gayriresmî dağıtımlarından checker framework’e, oradan da son dönemde JSpecify’a kayıyor
Bu tür temel parçaları dilin sağlaması gerekir; aksi halde parçalanma ve fiilî standart kütüphanelerin çoğalması kaçınılmaz olur
Kütüphaneleri yürütücüden bağımsız yazmak çok zor değil ama sürekli dikkat gerektiriyor ve topluluğun büyük kısmında bunun her zaman korunduğu söylenemez
Harika bir yazı. Bu tür optimizasyon derinlemesine analizlerini seviyorum ve proje hedeflerinin de başarıya ulaşmasını umuyorum
Derleyicinin “önemsiz” görünen durumları optimize etmeye çoğu zaman fazla emek harcamadığını düşündüğüm oldu
Yine de başlık, içeriğe göre fazla dramatik. “Async Rust Optimizations the Compiler Still Misses” olsaydı da tıklardım
Artık trait’lerde ve closure’larda async kullanabiliyoruz ama bu, async makinesinin kendisinden çok tip sisteminin güncellenmesi demek. Waker da biraz daha kullanışlı hale geldi ama o da daha çok std/core tarafındaki iyileştirmelerle ilgili
Anladığım kadarıyla async Rust’ı hayata geçiren kişiler ciddi biçimde tükenmişlik yaşadı ve etkinlikleri azaldı; sonrasında da bunu devralan pek çıkmadı. Yine de Google tarafındaki kişilerin, yakalanan değişkenlerin bellek yerleşimini optimize eden bir PR açmış olması sevindirici
Ben ve iş arkadaşlarım async’i çok kullandığımız için belki de bunu ya doğrudan yapmamız ya da en azından başlatmamız gerekecek. “Bedava” burada daha çok yavru köpek sahiplenmenin “bedava” olması gibi
Bu yüzden başlığın biraz clickbait olduğu doğru ama yine de onu geri çekmeyi düşünmüyorum
Yazar küçük fonksiyonların ek yüküne gereğinden fazla takılmış gibi görünüyor. “panic” ve “returned” durumlarının ek yükünden rahatsız ama bunlar büyük meseleler değil
Yararlı async blokların çoğu yeterince büyük olduğundan hata durumu ek yükü arada kaybolur
Yetersiz inlining konusunda bir noktası olabilir. Ama çok sayıda etkinliği sınırlayan şey çoğunlukla her etkinliğin ihtiyaç duyduğu durum alanıdır
async genel olarak bana yeterince olgunlaşmamış bir fikir gibi geliyor. Sıradan kod zaten asenkrondu
Bir async işi beklemeniz gerekiyorsa thread hazır olana kadar uyur ve çekirdek bunu soyutlar. Sonra insanlar kodu mantıksal thread’ler halinde kurmaktan hoşlanmadıkları için olay tabanlı callback sistemleri eklendi; ardından callback’lerle akıl yürütmenin zor olduğu ve sıralı kontrolün daha iyi olduğu fark edildi
Bu yüzden thread’lerin doğru programlama modeli olduğunu düşünüyorum
Şimdi dil runtime’ları taşınabilirlik ve performans nedeniyle “green thread”leri tercih ediyor ama çoğu dil bunu düzgün sunmuyor. Bunun yerine async/non-async renk sorunu, zamanlama, öncelik, preemption olmaması gibi dertler çıkıyor. Bu, 1970’lerden bile kötü bir zamanlama ve süreç modeli
async kod da çoğu zaman ifade edebileceği eşzamanlılığın tamamını kullanamayacak şekilde yazılır. Örneğin “N tane G/Ç işini aynı anda başlat” yerine “her X için await process(x)” gibi yazılır
Ama thread dünyasında bu eşzamanlılık sorunu daha da büyür. Thread’ler doğaları gereği fazla ağırdır; bu yüzden eşzamanlılığı verimli ifade etmek zordur ve bunu iyileştirecek bir optimizasyon yönü de yoktur
Bu yeni bir ders değil. Work-stealing executor’ların, geleneksel thread’lere göre çok daha düşük gecikme ve daha tutarlı P99 verdiği uzun zamandır biliniyor. Apple’ın 2000’lerin başında GCD’yi yapma nedeni de buydu
Thread’ler, çekirdek zamanlayıcısına iş yükünü anlaması için gereken daha zengin bilgiyi vermez; çekirdek thread’leri de ince taneli eşzamanlılık elde etmek için gereğinden ağır bir mekanizmadır. Salt hesaplama yerine G/Ç ya da karma iş yüklerinde bu daha da kötüdür
Her programın bu performans düzeyine ihtiyacı yok, ama aynı çabayla daha yüksek performans çıtasına ulaşmak çok daha kolay; nitekim geleneksel yaklaşımın yetişmekte zorlandığı gecikme ve throughput değerlerine ulaşabiliyorsunuz
async’in yön olarak doğru olduğuna dair bir işaret de io_uring. Çekirdeğin yüksek performanslı G/Ç yaklaşımı olan io_uring, geleneksel thread ve sistem çağrılarından tamamen farklı; tamamlanma işleme modeli de async eşzamanlılığa çok daha yakın. Yine de async/await tek başına async görevler arasındaki ilişkileri ifade etmek için yeterli “renk” sunmadığından, bundan tam yararlanmak daha zor olabiliyor
En son coroutine/zamanlama koduyla uğraştığımda, hemen sonlanan bir thread oluşturup join etmek yaklaşık 200µs sürüyordu; kendi green thread’imi oluşturup zamanlayıp beklemek ise yaklaşık 400ns alıyordu
Birileri yine absürt derecede karmaşık bir async framework tasarlayana kadar 10 yıl beklemenize gerek yok. Herhangi bir sistem dilinde 20 satır assembly ile kendi green thread / stack’li coroutine yapınızı kurabilirsiniz
Bant genişliği odaklı kod optimizasyonu aslında zamanlama tasarımı meselesi. Klasik çok iş parçacıklı modelde zamanlamayı sınırlı biçimde kontrol edebilirsiniz; async modelde ise neredeyse kusursuz kontrol mümkündür
İyi optimize edilmiş bir async zamanlama, aynı bant genişliği ağırlıklı işte eşdeğer çok iş parçacıklı bir mimariden açık ara daha hızlı olabilir
Bugün yüksek performanslı kodun büyük kısmı bant genişliği ağırlıklıdır ve async de bu tür iş yüklerini daha kolay optimize etmek için vardır
Eşzamanlı işlemleri test ederken ve yarış koşullarının doğru ele alınıp alınmadığını doğrularken, callback’ler zamanlamayı kontrol etmenizi sağladığı için çok daha kullanışlıdır. Her callback ayrı bir birimi temsil ettiğinden hangi olayların yer değiştirebileceğini görebilir ve farklı sıralamaları daha rahat inceleyebilirsiniz
Thread’lerde ise sıralamayı göz ardı etmek kolaydır; ayrıca başka thread’lerdeki karmaşıklığın mevcut thread’i ne zaman etkileyebileceğini düşünmemeye meylederiz. Bu sadelik değil, daha çok aşırı basitleştirmedir
Ayrıca yapay bariyerler ekleyip thread’leri durdurmadıkça ya da G/Ç’yi stub’larla değiştirip sıralamayı kontrol eden callback’li mock’lar geçmedikçe eşzamanlı senaryoları gerçekten değiştirip test etmek zordur
Callback’lerin asıl sorunu, yakalanan çağrı yığınının mantıksal çağrı yığını olmamasıdır. Bazı kütüphaneler/runtime’lar bunu anlamlı kılmak için uğraşsa da, iyi hata tanımları gerekir
Elbette iki paradigmayı karıştırıp ikisinin de sadece kötü yanlarını alma ihtimaliniz de var
Rust’ın ana hedefi güvenlikse neden panic olduğunu anlamıyorum. Kodda panic’e yol açabilecek hiçbir yol olmadığını ispatlayabilmeliyiz
Bu hafta boyunca buna baktım ve asla panic etmeyeceği garanti edilen bir program yazmanın çok zor olduğunu gördüm. Anladığım kadarıyla panic handler yaklaşık 300KB ve bunu dışarıda bırakmanın tek yolu, derleme anında kodda panic edebilecek hiçbir yol olmaması. Derlemeden sonra panic handler’ın ikili dosyada yer alıp almadığını kontrol etmek biraz hack gibi hissettiriyor
unwrap ve diğer panic işlemlerini lint’lerle engelleyebilirsiniz ama eğer no-panic Rust alt kümesi olsaydı, bu yazıda değinilen sorunların önemli bir kısmı ortadan kalkardı
Gerçekte bit flip gibi durumlar dışında yaşanmayacak şeyler için bile teorik olarak panic edebilen çok fazla işlemin olması sinir bozucu. Bir dizinin boş olmadığını ispatlamak ya da async ile uğraşmak da aynı şekilde
Sonunda ya hiç olmayacak durumlar için tonla hata işleme yazıyor ya da ilk alan ile geri kalan listeyi ayıran boş olmayan liste deseni gibi tuhaf yapılara başvuruyorsunuz. Üstelik bunlar da kendi şişkinliklerini getiriyor
Bir dizinin boş olmadığını kanıtlama gibi şeyler dâhil, kanıta dayalı kullanımı artırmaya yönelik işler de yavaş yavaş ilerliyor
panic olmaz ve her durumda yürütmenin devam etmesi gerekirse, invariants bozulmuş bellek bozulmaları gibi durumlarda toparlanabilmek için invariant kontrolü yapılan her yere bol miktarda hata işleme eklemeniz gerekir
Bu da tam olarak kaygı duyduğunuz şeyle aynı türde bir sorun yaratır: neredeyse hiç olmayacak durumlar için devasa hata işleme yükü
Aracın her şeyi hatasız hale getirmesini bekleyip kendiniz bir şey yapmaya yanaşmama tavrı yorucu geliyor. Kolay API istiyor insanlar; o yeterince kolay değilse YAML ile “programlanan” Kubernetes container’larını, o da kolay değilse GCP ya da Amazon’un tıkla-kullan barındırma hizmetlerini istiyorlar
Sonunda programlama yapmak değil, hata vermeyen uygulamalar tüketmek ister hâle geliyorsunuz; o yaşam tarzı da bir şeyler üreten insanlarla kurulan simbiyotik ilişkiye dayanıyor
Bu tür çirkin ama gerekli tartışmalar C++ tarafında da bir süredir yaşanıyor
Rust’a async eklendiği andan itibaren onun bulaşıcı doğasından hoşlanmamıştım
Rust’ın başarılı olmasını istiyorum; böyle insanların sayısı artarsa Rust’ın geleceği de daha parlak olabilir
Yakın zamanda Rust async çalışmaya başladım; şu an yaşadığım başlıca sorun kod tekrarı
Hem asenkron API hem de blocking API desteklemek istediğiniz her fonksiyonu iki kez yazmanız gerekiyor.
maybe-asyncgibi bir şey güzel olurduBunu aşmak için maybe-async, bisync gibi crate’lere baktım ama hepsinde ya sorunlar ya da ciddi kısıtlar vardı
asyncya daconstgibi anahtar sözcükler açısından fonksiyonları jenerik yapmayı amaçlayan keyword generics çalışması sürüyorŞu anda hem senkron hem asenkron dünyada yaşamak isteyen kod için en iyi seçenek sans-io. Fireguard’dan Thomas Eizinger bu desen hakkında güzel bir yazı yazmıştı[1]
Bu desen yalnızca sync/async sorununu temiz biçimde çözmekle kalmıyor, testleri de kolaylaştırıyor ve DST gibi tekniklere giden yolu da açıyor[2]
Benim de bu konuda bir yazım var[3]; orada sorunun yalnızca async ve sync ayrımı değil, farklı yürütücüler arasındaki daha geniş bir mesele olduğunu vurguluyorum
0: https://github.com/rust-lang/effects-initiative
1: https://www.firezone.dev/blog/sans-io
2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
3: https://hugotunius.se/2024/03/08/on-async-rust.html
asyncfonksiyon zatenmaybe-asyncfn -> voidilefn -> Futurearasındaki fark, ilkinin hemen sonuna kadar çalışması; ikincisinin ise daha sonra tamamlanabilmesiBir async fonksiyonu blocking biçimde çalıştırmak istiyorsanız, blocking bir yürütücü kullanmanız yeterli
Bu yazıyı sevmemin bir nedeni de 2026 Rust hedeflerine kadar uzanması
Ekipte Rust kullanıyoruz ama ihtiyaç duyduğumuz işleri yapmak için çok derine inmemiz gerekmedi. Yine de topluluk geri bildirimi yüksek olan bir dilin tabandan gelişmesini izlemek keyifli
C++’ta bunu pek hissetmedim; diğer alanlarda işlerin nasıl yürüdüğünü de pek bilmiyorum
Tek hoşuma gitmeyen nokta, her hedef için belirli bir finansman gerekiyor gibi görünmesi; biraz Kickstarter hissi veriyor. Şu an bulunan en iyi model gerçekten bu mu diye merak ediyorum
Proje hedefi, bir kişinin ya da küçük bir grubun belli bir işi yapmak istediğini belirtmesi ve Rust projesi gönüllülerinden kod incelemesi ya da soruları yanıtlama gibi sürekli destek zamanı istemesi için kullanılan bir sistem
Bu, Rust projesinin o hedefi resmen benimsediği ya da mutlaka desteklediği anlamına gelmiyor
Bu yüzden bunu Rust’ın resmî yol haritası gibi görmek doğru değil; daha çok “bu alanda çalışmak isteyen katkıcılar var” şeklinde anlamak gerek
Bir teknoloji ticari olarak yerleşince ne yazık ki işlerin bu yöne kayması normal görünüyor. Büyük sponsorların yalnızca ilgilendikleri alanları finanse etmesini eleştirmek de kolay değil
Neyse ki TweedeGolf’un kayda değer finansmanının Hollanda hükümetinden geldiğini biliyorum
Yeni özellikler “satılabilir”. Geliştirmek para ister ama gerçek sorunları çözer; eğer o sorunun maliyeti özellik geliştirme maliyetinden büyükse şirketler genelde ödeme yapmaya razıdır
Bakım daha zor ama artık bakım fonları da var. Örnek olarak RustNL fonu: https://rustnl.org/maintainers/
Bu tür fonlar daha geniş ve sürekli işleri destekler; çeşitli kuruluşlar az az katkı sunarak bunları ayakta tutar
Bunun en iyi model olup olmadığını bilmiyorum ama en azından bir ölçüde çalışıyor gibi görünüyor
Rust Async ve Tokio belgelerini okursanız, CPU yoğun bölümleri neden async yığınına koymamanız gerektiği,
std::sync::Mutexgibi temel araçları async bloklarda nasıl verimli kullanacağınız ve senkron kod ile async kodu nasıl birleştireceğiniz gayet iyi anlatılıyorBirçok kod tabanı verimlilikle ilgilenmiyor ya da buna ihtiyaç duymuyor, bu yüzden bu yönergeleri izlemiyor. Ama performans ve verimliliği önemseyen çok sayıda proje var ve kod üretimde çalışmaya başlayınca bu tuzaklar fark ediliyor. ScyllaDB bunun bir örneği
LLM’ler de yardımcı olmuyor. Her şeyi
maine kadar async üretiyor, yanlış temel araçları kullanıyor ve sistemi doğru tasarlamıyorlarYinelemeli durum katlaması, yani
process_commandörneğindeki gibi match’i await dallarının dışına çekme deseni, bugün mevcut async koda herkesin uygulayabileceği en kolay yöntemDerleyici çalışması gerektirmiyor; yalnızca refaktör etmek yeterli
“Future’lar kolay inline edilmez” kısmıyla ilgili olarak, kendi yaptığım bir programlama dilinde async fonksiyon içindeki async fonksiyon çağrılarını inline eden özel bir pass yazdım
Genel olarak iyi çalışıyor ve bazı boilerplate’leri ortadan kaldırıyor, ama ortaya çıkan ikili dosya boyutu ciddi biçimde büyüyor
Teknik olarak Rust da aynısını yapabilir
Lobste.rs görüşleri
Sadece başlığa bakınca beklediğimden çok daha yapıcı bir yazıymış
Umarım bu işi yapmak isteyen kişi ihtiyaç duyduğu desteği alır
Bu sorunun ele alındığını görmek güzel. Şu anda rustc’nin LLVM’e fazla miktarda kod verdiğini ve optimizer’ın her şeyi halletmesini beklediğini söyleyen birkaç yazı görmüştüm; özellikle bu yazı bu iş için finansman da talep ediyor
Aman Tanrım, ben aptalmışım
async’in bir şekilde runtime, iş takibi ve tamamlanmayı kontrol eden polling gerektirdiği için özünde hep “şişkin” olduğunu düşünüyordum. Sonuçta bu overhead sıfır değil
Burada sözü edilen “sıfır maliyetli soyutlama”nın dil özelliğiyle ilgili olduğunu, sonradan eklenen runtime’dan ayrı düşünmek gerektiğini varsayıyordum
LLVM’e vermeden önce rustc’nin gerçekte ne ürettiğine bakmayı hiç düşünmemişim
async Rust’a aşina olmayanlar için:
Bu gerçekten doğru. İç içe geçmiş async çağrı ağaçları, maksimum optimizasyondan sonra içinde durum makinesi bulunan tek bir struct hâline kadar katlanabiliyor. Gerçekten çok zekice bir yaklaşım
Release build’de bu duruma gelindiğinde bir tür deadlock mu oluşuyor? Yoksa sürekli
Pendingdönen bir işi bekleyen task’lar yüzünden sızıntı da olabilir mi?.awaitile yanlış polling yapılamazAklıma birkaç şey geliyor:
panic=unwindtercihini sevmiyorum. Bazı test harness’leri dışında,panic=abortyerine bunu seçmenin maliyeti karşılayacak kadar büyük bir faydasını neredeyse hiç görmedim. Hatta test harness için bile Linux’tapthread_joinyerine çalışan thread’iwaitetmek için tuhaf bir şekildeclonekullanılarak benzer bir tercih yapılabileceğini düşünüyorum. Bu noktada yanılıyor olabilirimLink başkasında da az önce bozuldu mu?
Düzeltme: blog yazısı yaklaşık yarım saniye görünüp sonra 404 sayfasına düşüyor
Düzeltme 2: Blog yazıları listesine gidip etrafa tıkladım; listede duran o yazıyı açınca da 404 sayfasına gidiyor. Statik bir sayfa olan ya da en azından öyle olması gereken bir blogu nasıl bu kadar bozabilirsiniz?
Bu arada aynı yeniden üretim adımlarını izledim sanırım ama bende hiç 404 çıkmadı. Telefonda ve masaüstünde, JavaScript açıkken de kapalıyken de denedim. Yani yaşadığınız şey göründüğünden daha karmaşık olabilir