2 puan yazan GN⁺ 2023-09-09 | 1 yorum | WhatsApp'ta paylaş
  • Rust’ın async/await yapısı, on binlerce bağlantıyı ele alan büyük ölçekli eşzamanlılığı hedefliyor; ancak düşük seviye kontrol ve statik yaşam süresi doğrulaması gibi Rust hedefleriyle çatışarak normal Rust’tan farklı bir geliştirme deneyimi yaratıyor
  • Thread ve channel birçok yazılım için yeterli olsa da, C10K ölçeğinde bağlantı başına thread yaklaşımının yükü büyüyor ve bu da kullanıcı alanı task’leri ile çalışma zamanı zamanlamasını gerekli kılıyor
  • async Rust’ta verilerin Send ile taşınması ya da 'static referanslar olarak ele alınması gerekiyor; ayrıca async’in bulaşıcı yapısı yüzünden bu kısıtlar kod tabanının geneline tekrar tekrar yayılıyor
  • Arc, derleme sorunlarını çözse de nesnelerin ve kaynakların yaşam süresini belirsizleştiriyor; özyinelemeli async, future ile task arasındaki fark ve blocking çağrıların çalışma zamanı thread’lerini durdurması gibi tuzaklar da devam ediyor
  • Haskell veya Go’da “async code” normal kod gibi çalışıyor; çalışma zamanı ve GC farkları gizlediği için, bu tür programlamada Rust’ın açık kontrol yaklaşımı her zaman net bir avantaj olarak işlemeyebilir

Eşzamanlılık ve paralelliğe neden ihtiyaç var

  • Hızlı programların aynı anda karşılaması gereken iki gereksinim vardır
    • Birden fazla CPU çekirdeğini kullanarak bilgisayarın tamamından yararlanmak gerekir
    • İnternet üzerinden mesaj gönderme veya dosya açma gibi yavaş işlemler beklenirken başka işler yapılmaya devam edilmelidir
  • Paralellik, kodun birden fazla CPU’da aynı anda çalıştırılması problemidir
  • Eşzamanlılık, problemi bağımsız parçalara ayırma biçimidir
  • Bunlar aynı şey değildir; ancak programı eşzamanlı parçalara bölmek, bu parçaların paralel çalışmasını ve çekirdeklerin sürekli meşgul kalmasını sağlayabilir

Process, thread ve channel

  • Eşzamanlı sistem kurmanın basit bir yolu, kodu birden fazla process’e bölmektir
    • İşletim sistemi zamanlayıcısı, çalışmaya hazır process’lerin zaman dilimlerini kullanılabilir CPU çekirdeklerinde yürütür
    • Shell komutlarını pipe ile bağlama durumunda da bu model kullanılır
  • Process yaklaşımında process’ler arası iletişimin maliyeti yüksektir
    • Birçok uygulamada verinin OS belleğine kopyalanıp sonra geri alınması gerekir
    • Paylaşımlı bellekle maliyet azaltılabilir, ancak OS’nin process’leri birbirinden yalıtma avantajı da zayıflar
  • Thread’ler aynı belleği paylaşarak bu ek yükten kaçınır; ancak mutex, condition variable, semaphore gibi eşzamanlama araçları yanlış kullanılırsa veri yarışı ve deadlock ortaya çıkabilir
  • Tony Hoare’un Communicating Sequential Processes modeli, thread’leri kuyruklar veya channel’lar ile bağlar
    • Thread’ler belleği paylaşmadığı için process benzeri bir yalıtım elde edilir
    • Her thread’in girdi ve çıktısı channel’lar üzerinden görünür olduğundan akıl yürütmek ve hata ayıklamak kolaylaşır
    • Channel’ların kendisi eşzamanlama görevi görür; boşsa alıcı bekler, doluysa gönderici bekler
  • Rust standart kütüphanesinde std::sync::mpsc::sync_channel bulunur
  • Birçok yazılım için thread ve channel ile CPU yoğun döngüleri paralelleştiren Rayon gibi araçların birleşimi yeterlidir

Kullanıcı alanı eşzamanlılığı ve Rust async

  • On binlerce eşzamanlı kullanıcının bağlandığı web sunucuları gibi C10K problemlerinde, bağlantı başına bir thread atama yaklaşımı sınırlarına ulaşır
    • Linux’ta her thread 4kB’lik bir control block taşır ve thread geçişleri, işletim sistemi zamanlayıcısına giren bir context switch gerektirir
  • Büyük ölçekli eşzamanlılık için bazı diller task’leri kullanıcı alanında oluşturur ve yönetir
    • Çalışma zamanı bu task’leri bir OS thread havuzu üzerine zamanlar
    • Genellikle havuz, CPU çekirdeği başına bir thread düşecek şekilde yapılandırılarak paralellik en üst düzeye çıkarılır
    • Bu yaklaşım green thread, lightweight thread, lightweight process, fiber, coroutine gibi adlarla anılır
  • Rust, C# veya Node.js’te görülen async/await modelini kullanır
    • async fn, doğrudan değer döndürmek yerine sonucun .await ile alındığı bir future veya promise döndürür
  • Rust future’ları, işbirlikçi zamanlama ve stackless tasarım sayesinde çok küçük ve hızlıdır
  • Rust, future soyutlamasını sunarken aynı zamanda programcıya düşük seviye kontrol vaat etmeye çalışır
    • Tüm nesnelerin ve referansların yaşam süresini derleme zamanında statik olarak doğrulamayı hedefler
    • Future’lar kodu ve kodun referans verdiği verileri binlerce parçaya böler; bunlar çalışmaya başladıktan sonra ancak anlaşılabilecek koşullara göre, herhangi bir anda herhangi bir thread’de yürütülebilir
    • İstemci verisini okuyan bir future yalnızca ilgili sokette okunacak veri olduğunda çalıştırılmalıdır; fakat lifetime annotation bunun ne zaman olacağını söylemez
  • Rust, future çalışma zamanını dile gömmez; bunu Tokio gibi kütüphanelere bırakır
    • Böylece kullanıcılar ortamlarına uygun alternatifleri seçme özgürlüğü kazanır
    • Ancak Tokio’nun dile gömülü olduğu bir dünya hayal edilse bile aynı kurallar geçerli olacağından, bu tartışmada ikincil bir ayrıntıdır

Send, 'static ve Arc’ın yarattığı baskı

  • Derleyiciyi ikna edebilmek için verilerin ya Send olarak işaretlenip taşınması ya da 'static yaşam süresine sahip referanslar üzerinden aktarılması gerekir
  • async kodda birden fazla task’in ortak durumu paylaşması yaygındır; bu yüzden veriyi kopyalamadan taşıma yaklaşımı sık sık uygun düşmez
  • Referanslar da zordur; ayrıca future’ın yaşam süresini “sonsuz”dan daha kısa sınırlayan thread::scope benzeri bir karşılık yoktur
  • async bulaşıcıdır; async fonksiyon çağıran fonksiyonun da async olması gerekir
    • Bu nedenle yaşam süresi ve taşınabilirlik sorunları birkaç fonksiyonda değil, kod boyunca sürekli çözülmek zorundadır
    • Zincir, çalışma zamanında future tamamlanmasını block_on ile bekleyerek kırılabilir; ancak bu yaklaşım birleştirilebilir değildir ve iç içe kullanıldığında çalışma zamanı panic üretebilir
  • Arc, dinamik yaşam süresini birden çok thread arasında ele alma aracıdır; borrow check’i geçmeye ve kodun derlenmesine yardımcı olur
  • Ancak Arc’ın yaygın kullanımı nesnelerin ve kaynakların yaşam süresini bulanıklaştırır
    • Bellek, dosya, soket gibi kaynakların ne zaman serbest bırakılacağı net olmaz
    • Gerçek GC’nin sunduğu allocation throughput, düşük fragmentation ve cycle leak önleme gibi avantajlar olmadan, GC’ye benzer kayıplar yaşanır

async Rust’ın ek tuzakları

  • Rust coroutine’leri stackless olduğu için derleyici her coroutine’i .await noktasına kadar ilerleyen bir durum makinesine dönüştürür
    • Özyinelemeli async fonksiyonlar özyinelemeli tanımlanmış tiplere dönüşür
    • Sadece kendini çağırmak isteyen kullanıcı, bunu elle boxing yaparak ya da async-recursion gibi bir crate kullanarak çözmek zorundadır
  • Future, await edilene kadar hiçbir şey yapmaz
  • Task, çalışma zamanının thread havuzunda işi başlatır ve tamamlandığını bildiren bir future döndürür
  • Future içinde blocking kod çağrılmasını engelleyen bir mekanizma yoktur
    • Böyle bir çağrının üzerinde çalıştığı runtime thread’ini bloke etmesi de engellenmez
    • Bu durum, async kullanmanın temel amacıyla çelişir

Normal Rust, Haskell ve Go arasındaki fark

  • async Rust, “normal” Rust’tan belirgin biçimde farklı bir his verir
    • Daha fazla tuzak içerir
    • Anlaması ve öğretmesi daha zordur
  • Kullanıcılar iki seçenek arasında kalır
    • Soyutlamanın gerçekte nasıl çalıştığını derinlemesine anlayıp karmaşık kod yazmak
    • Arc, Pin, 'static gibi öğeleri kodun her yanına serpiştirip işe yaramasını ummak
  • Deneyimli geliştirici ekipleri bile yeni bir projede Rust kullanmaya çalışırken bu ayrıntılara takılabilir
  • Haskell veya Go’da “async code”, normal koddur
    • Bu iki dil, blocking ve non-blocking kod arasındaki farkı kalın bir çalışma zamanı katmanının arkasında gizler
    • Yaşam süresi sorunları garbage collection’a bırakılır
  • Bu tür büyük ölçekli eşzamanlı kullanıcı alanı yazılımlarında, çalışma zamanı ve GC’nin farkları gizlemesi başlı başına bir avantaj olarak işler
  • Rust, büyük ölçekli eşzamanlı kullanıcı alanı yazılımları için iyi bir araç olmayabilir; böyle gereksinimleri olmayan projelerde kullanılması daha uygun olabilir

1 yorum

 
GN⁺ 2023-09-09
Hacker News yorumları
  • Yüksek performanslı bir metaverse istemcisini Rust ile yazıyorum; şu anda yaklaşık 40 bin satır civarında.
    Demo videosu https://video.hardlimit.com/w/tp9mLAQoHaFR32YAVKVDrz adresinde.
    Düzgün bir metaverse, kullanıcıların ürettiği içeriği neredeyse gerçek zamanlı işlemek zorunda olduğundan, benzer oyunlara kıyasla 2–3 kat VRAM, sunucudan asset yüklemek için yüzlerce Mbps bant genişliği, birden fazla CPU ve render ile GPU yüklemelerini paralel yürütecek Vulkan gerekiyor.
    Bu, “web ölçeği” eşzamanlılıkta olduğu gibi aynı adres alanında küçük sunucuların ayrı ayrı çalıştığı bir yapı değil; yüksek öncelikli render thread’i, ağ olaylarını güncelleyen thread, asset yükleme ve sıkıştırma açma thread’leri, hareketli nesneler, LOD ve önbellek temizliği gibi işleri üstlenen çok sayıda thread’in birlikte hareket ettiği bir yapı.
    Rust’ta sabitler dışında küresel durum kullanmadan epeyce kilit kullanıyorum; kanalları uygun yerlerde kullanıyorum ve ana nesne ağacını tekil sahiplikle çoğunlukla güncelleme thread’i yönetiyor. Grafik nesnesi bağlantıları Arc referans sayacıyla yönetiliyor; mesh ve texture’lar Rend3/WGPU/Vulkan üzerinden GPU’ya yükleniyor.
    Bunu C++ ile yapsaydım sürekli crash’lerle boğuşurdum; Rust’ta bellekle ilgili crash yılda yaklaşık bir kez oluyor ve o da genelde başkalarının unsafe kodundan kaynaklanıyor. Kendi kodumda unsafe kullanmayı yasakladım; derlemek zor, ama bir kez derlenince “öylece çalışıyor” denebilir, bu yüzden eşzamanlılık debug’ından çok daha iyi olduğunu düşünüyorum.
    Şikâyetlerim de var. Rust veri yarışlarına karşı güçlü, ama deadlock’ları engelleyemiyor; bu yüzden çağrı yolları boyunca kilit sırasını izleyen bir statik analiz aracı gerekiyor. async, hesaplama ağırlıklı işler ve farklı önceliklere sahip thread’ler için uygun değil, ama sürekli bağımlılıklara sızıyor. Tekil sahipliğe ters referansların eklendiği yaygın yapılar Rc ve Weak olmadan aşırı zor; trait sistemi de karmaşık olduğundan, nesne yöneliminin doğal olduğu asset işleme kısımlarında yinelenen kod ortaya çıkıyor.
    Grafik tarafındaki temel crate’ler de henüz yeterince olgun değil. “Rust’ta 5 oyun ve 50 oyun motoru var” sözü dilin değil ekosistemin sorunu; https://gamedev.rs/ ile karşılaştırınca bile Rust ile ciddi oyun geliştirmenin hâlâ yetersiz olduğu görülüyor. Takvimi olan profesyonel oyun geliştirme için Rust oyun ekosistemi henüz hazır değil; kabaca 5 kişinin bir yıl kadar daha çalışması gereken seviyede olduğunu düşünüyorum.

    • Son 3 yıldır robot simülatörünü Rust ile geliştiriyorum; deneyimim neredeyse aynı. 3 yılda gerçek runtime bug’ı yaklaşık 5 taneydi; Rust ve async ile ilgili sorunlar olsa da genel olarak avantajları çok daha büyük.
    • Kilit sırasını izleyerek olası deadlock’ları bulmak iyi bir fikir gibi görünüyor.
      Linux’taki lockdep gibi, bir kilit tutulurken başka hangi kilitlerin alındığını analiz edip, gerçekten durmadan önce bile riskli kombinasyonları bildirebilir. Karmaşık kilitlerde “bu kilit sınıfı her zaman adres sırasına göre alınır” gibi açıklamalar gerekebilir, ama uygulanabilir görünüyor.
    • Bir MMO’da neredeyse aynı işi Java ile yapıyorum ve JDK bunu çok kolaylaştırıyor. Ağdan model oluşturup nesneleri eşzamanlı bir kuyrukla UI thread’ine taşımak yeterli; oldukça sıkıcı denecek kadar basit, ama hızlı.
    • Rust’ta yarış koşulları yok değil; veri yarışı yok.
      Veri erişiminin dışında hâlâ yarış koşulları oluşabilir: https://news.ycombinator.com/item?id=23599598
    • Öncelik sorunu görece kolay çözülebilir.
      Birden fazla thread pool oluşturup future’ları uygun şekilde yönlendirebilir ya da kendi event loop’unuzu yazarak farklı önceliklere sahip birden çok event queue’dan çekebilirsiniz. İkinci yöntem, işlerin çalışma süresi sınırlıysa CPU %100 doluyken bile düşük öncelikli işleri ilerletirken yüksek öncelikli işlere soft real-time garantisi verebilir.
  • async Rust konusunda tuhaf bir noktadayız.
    Arc, RwLock ve bolca paylaşılan durum kullanınca işlerin kirlendiği; özellikle 'static her yere yayılmaya başladığında renkli fonksiyonlar gibi her şeyi enfekte ettiği eleştirisi doğru. Eskiden Arc ekleyip lifetime borrowing’i akıllıca yönetmeye çalışırken ortalığı batırmıştım.
    Ama Rust’ta kanallar da var. Şu anda yazdığım kodların çoğu, birkaç görevin kanalları servis ettiği; gelen mesaja bakıp gerekirse başka bir göreve gönderilecek mesajı uygun kanala koyduğu bir yapıda. Nesne paylaşmıyorum. Birden fazla görev büyük bir nesneye ihtiyaç duyarsa, ilgili sorgu sonuçlarını mesaj olarak gönderen bir görevin içinde tutuyorum ya da her görevin mesaj akışında kendi kopyasını oluşturmasını sağlıyorum.
    Buna rağmen Arc’ın nasıl kullanılacağı ve lifetime’ların nasıl ele alınacağı üzerine çok fazla yazı var. Bir async runtime’ı uyguluyorsanız gerekli olabilir, ama ortalama bir kütüphane kullanıcısının neden buna bu kadar odaklanması gerektiğini pek anlayamıyorum.

    • Eleştiri bana biraz tuhaf geliyor. async doğrudan çok thread’li olmak demek değil; aynı thread içindeki async ise paylaşım olmadığı için paylaşılan şeylerin tamamına sihirli anahtar kelimeler eklemek de gerekmez.
      Thread’ler arasında geçiş yaparken bolca paylaşılan durum tutmak yerine kanallarla sinyal gönderilir. Mutlaka gerekli bir küresel durum varsa Arc/RwLock gibi özel erişim mekanizmalarını saran küçük bir struct oluşturup, çağıran açısından basit bir fonksiyon çağrısı gibi görünmesini sağlarsınız.
      Send+Sync konusundaki endişeyi de pek anlamıyorum. Deneyimime göre çoğu şey kolayca Send+Sync oluyor; olmayanlar da öyle olmaması gereken ya da olamayan şeylerdi. Ayrıntıları düşünmeden kod yazmak istediğim zamanlar da oluyor, ama verimli eşzamanlılık ve paralellik gerekiyorsa mikrosaniyeler ve throughput önemli hâle gelir; o noktada gerçek bilgisayar kodunu düzgün yazmanız gerekir.
    • Mesaj iletimi paradigması gerçekten iyi; Erlang gibi dillerin dağıtık sistemlerde harika bir seçim olduğunu gösterdi.
      Ama bu tarz kodlama, senkron koda green thread eklenmiş gibi hissettiren async JavaScript’ten epey farklı. İnsanlar alıştıkları şekilde kod yazmaya çalıştıkları için Rust’ta Arc ve RwLock yoluna sapıyor gibi görünüyor.
    • Smalltalk ve gerçek nesne yönelimli programlama rüyası hâlâ yaşıyor.
    • Üniversitedeyken bir hocamdan bu tavsiyeyi öğrenmiştim; gerçekten çok faydasını gördüm.
      Problemi görevler arasında akan veri olarak yapılandırmak, kuyruklarla bağlamak ve paylaşılan durumdan kaçınmak, hangi dili kullanırsanız kullanın çok thread’li programlamayla başa çıkmanın daha iyi bir yolu.
    • Bilge bir programcının dediği gibi: “Belleği paylaşarak iletişim kurmayın; iletişim kurarak belleği paylaşın.”
  • async aslında çok daha zor bir Rust; gerçekten ihtiyaç duyan projeler muhtemelen %1 civarındadır, ama neredeyse herkese dayatılmış gibi olması üzücü.
    Yine de o %1 içinde gerçekten harika. linkerd veya nginx gibi çok sayıda ağ çağrısını merkezinde işleyen servislerde, oyunlarda muazzam sayıda hafif iş çalıştırılan durumlarda, gömülü sistemlerde işbirlikçi eşzamanlılık gerektiğinde async Rust güçlü bir silaha dönüşüyor.
    Sistem ve uygulama düzeyindeki kodların çoğunda asenkron giriş/çıkışa gerek yok. REST uygulamaları için thread pool yeterli; async gerekse bile bunu ağ gibi küçük bir kısımla sınırlayıp geri kalanını thread’ler ve channel’larla bağlayan karma model genelde doğru olanı.
    Rust topluluğu asynci her yerde fazla düşünmeden kullandığı için, kullanıcı deneyimi daha iyi olan blocking I/O Rust ekosistemde ikinci sınıf vatandaş hâline geldi. Web framework’lerinde de Axum, Warp gibi iyi tasarlanmış birkaç asenkron framework var; blocking tarafta ise tiny_http, rouille, astra gibi seçenekler çok daha sınırlı.

    • Temel mesele Rust’ın coroutine’leri yanlış uygulamış olmasında.
      Stack’siz coroutine’ler seçilince async/await ve renkli fonksiyon problemi ortaya çıktı; yazıda bahsedilen sürtünme de bundan kaynaklanıyor. Go stack’li coroutine kullandığı için bu sorun yok.
      Rust da başlangıçta stack’li coroutine’leri değerlendirdi, ancak coroutine preemption runtime’ı gerektirdiği ve maliyetli olduğu düşünülerek stack’siz modele gidildi. Ama çoğu kişi runtime’sız async Rust kullanmıyor, Tokio kullanıyor; Tokio da kaçınılmak istenen runtime’ın yaptığı işleri fiilen neredeyse tamamen yapıyor.
      Böylece async Rust kullanıcılarının çoğu iki dünyanın da kötü yanlarını bir arada almış oluyor. Gömülü tarafta çok ince bir runtime ile async Rust kullananlar da var; ama sayıları az ve onların bile tamamen ikna olduğu söylenemez.
    • Programıma Tokio’nun yine bağımlılık olarak çekildiğini gördüm. Doğrudan kullanmıyorum bile; bir crate’in kullanmadığım bir fonksiyonu reqwesti getiriyor, o h2yi getiriyor, o da tekrar tokioyu getiriyor.
    • Platform virtual thread destekliyorsa async kullanmak için bir sebep olup olmadığını merak ediyorum.
      Java kullanan biri olarak, tüm asenkron paradigmayı bırakıp blocking’in sorun olmadığı virtual thread’ler üzerindeki blocking modele göre kodu yeniden yazmaya çalışıyorum.
  • async o kadar çok crate’e yayılmış durumda ki programın tamamı async oluyor ya da en azından pek çok iş için Tokio’ya bağımlı hâle geliyor.
    Web sunucusu istiyorsan “async + tokio kullan ya da defol” deniyormuş gibi; SQL connector’larında da asenkron istemiyorsan kendin yazman gerekiyormuş havası var. Herkes asyncin getirdiği sorunları farklı bir şekilde çözüyor; async closure gibi şeyler ise derleyiciye cehennemin kapılarını açıyor hissi veriyor.
    Rust’ın kendisinin ve derleyicinin sorun çözmeye yardımcı olması güzel, ama ekosistemin “async değilse kendin yap” noktasına yakın olması yeterli değil.

    • Standart kütüphanede veya futures crate’inde daha iyi asenkron temel bileşenler olsaydı acı çok azalabilirdi.
      Executor’ın uygulaması gereken trait’ler veya senkron koddan asenkron kod çalıştıracak temel bir blocking executor gibi şeyler gerekiyor. Şu anda birden fazla asenkron runtime’ı destekleyen kütüphane yazmak bile eziyet; sonuçta yalnızca Tokio destekleniyor ya da en fazla async-std ekleniyor.
  • async Rust uzmanı değilim, ama bu ay birkaç bin satır senkron Rust yazarken şunu hissettim: rustc bir yaklaşımı zorlaştırıyorsa bunun genelde iyi bir nedeni oluyor ve benzer sonucu daha iyi bir yolla elde etmenin bir yolu bulunuyor.
    Dili öğrenme aşamasındaysanız önce sıradan senkron kod, döngüler ve koşullar ile borrow kurallarına alışmanızı öneririm. async, yalnızca uygulama tarafında değil, “asenkron nedir ve kullanıcıya nasıl görünmelidir” gibi felsefi düzeyde de hâlâ epey gelişim içinde.
    Derleyici trait’lere büyük ölçüde dayanıyor, ancak trait’lerin asynci ele alma özellikleri stabilize edilmiş değil. Örneğin https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-i... gibi çalışmalar var.
    Trait’lerdeki asenkron özellikler stabilize edilmemişse, Rust’ın asenkron kodu henüz güzel değil diye saldırmak, sonunda tamamlanacak bir kitabın ilk taslağını eleştirmeye benziyor.

    • “İyi asenkron API tasarımı”nın ne olduğunu merak ediyorum. Tamamen asenkron odaklı gidip ölçeklenebilir, bakımı yapılabilir ve anlaşılması kolay bir sunucu tasarlansa nasıl görünmeli?
      Asenkronluğun tüm codebase’e yayılması da nasıl engellenebilir, bunu düşünüyorum.
      Şu anki fikrim, giriş/çıkış thread’inin liburing veya epoll sistem olaylarını “submit” ve “handle” diye iki aşamaya ayırıp başka bileşenlere gönderen bir yapı. Örneğin bir tcp-connection oluşturduğunuzda “yazmaya hazır”, “okumaya hazır” gibi asenkron olaylara abone olabiliyorsunuz; yazmaya hazır olayı, normal bir mutex ile doldurulmuş buffer’dan veriyi çıkarıp EPOLLOUT/io_uring_prep_writev ile gönderiyor.
      Thread’ler arası olay iletimi için LMAX Disruptor deseninin çok üreticili·çok tüketicili ring buffer’ı kullanılabilir. Uygulama thread’i veya thread pool, her biri kendi event loop’una sahip olup bu ring buffer’ı işler.
      Asenkron olayların tetiklenme sırasını ifade eden bir sözdizimi üzerinde de çalışıyorum; Bash pipeline’ına benziyor ve statelines adını veriyorum: initialstate1 initialstate2 = state1 | {state1a state1b state1c} {state2a state2b state2d} | state3
    • Stabilize edilmediyse production’da da kullanılmamalı.
    • Yazarın Rust’a yeni başladığını varsayan yorumlar ilginç. Aksine, yazarın deneyimi onlarınkinden daha fazla olabilir.
  • Arc’nin yaşam süresi bilinmez değildir; nerede ve nasıl tutulduğuna göre belirlenir
    Bu yazıdaki kopukluk, yazarın Rust’ı öğrenip dilin çalışma biçimine uyum sağlamasından ziyade, çöp toplama gibi önceki düşünce modellerini Rust’a zorla uygulamaya çalışmasından kaynaklanıyor gibi görünüyor. Yeni bir dil öğrenirken sık görülen bir tuzak, ama Rust insanı özellikle daha sık tökezletiyor

    • Bu anlamda bakılırsa, çöp toplamalı sistemlerde nesne yaşam süresinin de “referans verildiği sürece” diye bir alt sınırı vardır
      Ama bu, nesne yaşam sürelerini derleme zamanında statik olarak sınırlamaya çalışan ödünç alma denetleyicisinin hedefinin neredeyse tersidir
      Gerçekte durum neredeyse tersiydi. C, C++, Rust ile yaklaşık 10 yıl sistem programlama yaptıktan sonra mevcut işimde Haskell’i yoğun kullanmaya başladım; büyük bir dil runtime’ının ve çöp toplamanın bazı problem alanlarında bir canavar olmadığını görmek epey ufuk açıcı oldu
    • Eleştirinin önemli bir kısmı böyle hissettiriyor. async dönüşümünün, asenkron olmayan kodda derleyicinin yapabileceği optimizasyonları nasıl engellediğine dair bir yazı olacağını sanmıştım
      Weak ile boğuştuğu kısım, karmaşık bir sahiplik yapısı kurmaya çalışıyormuş gibi görünüyor; bu da Rust genelinde kolay bir iş değil. Ben zayıf akıllı işaretçileri çok nadiren kullanırım
      Kanallardan neredeyse hiç söz edilmiyor; oysa asenkron kodda veya asenkron ve senkron kod arasında bağlantı kurarken programın farklı bölümlerini haberleştirmenin başlıca aracıdırlar. Notify, semafor gibi sinyal soyutlamaları da var
      Mutex yavaştır ve kolayca darboğaz olur; paylaşılan durum da hızla karmaşıklaşır. Bu uzun zamandır bilinen bir gerçek. Sorun en başta BIG_GLOBAL_STATIC_REF_OR_SIMILAR_HORROR benzeri bir yapıda olabilir
      Asenkron bağlamda bloklayan kod çağırmayı engelleyemediği eleştirisi yerinde, ama gerekirse tokio::spawn_blocking gibi şeylerle nispeten yönetilebilir
    • Referans sayma da çöp toplamanın bir türüdür https://en.wikipedia.org/wiki/Garbage_collection_(computer_s...
      Yazarın Arc’ın ne olduğunu ve nasıl çalıştığını biliyor olma ihtimali yüksek; asıl nokta, Rust async içinde senkron koda kıyasla genel RAII yerine Arc’ı çok daha sık kullanmak zorunda kalmanız gibi görünüyor
      Program nesnelerinin %90’ı referans sayımlıysa, çok sayıda küçük heap ayırma/serbest bırakma ve atomik işlem maliyetine katlanmaktansa izlemeli çöp toplama kullanmak daha iyi olabilir. Tokio eğitimindeki örnek de benzer bir yöne işaret ediyor: https://tokio.rs/tokio/tutorial/shared-state
      Rust’ta gerçek bir izlemeli çöp toplamanın HTTP sunucusu gibi yaygın asenkron uygulamaları anlamlı biçimde hızlandırıp hızlandıramayacağını merak ediyorum: https://manishearth.github.io/blog/2015/09/01/designing-a-gc...
    • Arc’ın yaşam süresi rastgele değil, statik olarak bilinemez olandır
    • Rust’ın Arc’ı taşınabilir veya ödünç alınabilir; referans sayacına dokunmadan da kullanılabilir
      Birçok durumda, örtük referans sayımlı dillerdeki nesnelerden çok daha ucuzdur
  • Rust’ı seviyorum ama async karmakarışık; senkron kod yazar gibi asenkron kod yazamazsınız
    İkisini karıştırmanın kötü bir fikir olduğuna giderek daha çok ikna oluyorum; her şeyi senkron bırakıp yalnızca tek bir async kanal ilkel öğesi sunan Go tarzı yaklaşım doğru olabilir
    Şu anda Future uygulayan bir struct içinden senkron metot çağıracak şekilde mantığı tesisatlandırıyorum; oldukça ilginç bir meydan okuma. Sıfır maliyetli asenkron soyutlamaları kullanıcı için bir ölçüde kolaylaştırabilirsiniz, ama acıyı kütüphane geliştiricisi üstlenir

    • Son kısma katılmıyorum. async son kullanıcı için de kesinlikle sancılı; Rust’ın temel özellikleri olan yaşam süreleri ve açık tiplerden yoksun ayrı bir dil kullanıyormuşsunuz ve üzerine bolca Pin serpiştirilmiş gibi hissettiriyor
      Kapsamlı fiber’lar çalıştıramadığınız için sonunda her yere Arc iliştirmek zorunda kalıyorsunuz; Pin ise unsafe olmadan kullanması zor ve asenkron bir fonksiyondaki çok küçük bir değişiklik, tüm kod tabanındaki future’ları !Send hâline getirebiliyor
    • Kütüphane geliştiricileri karmaşıklığı kullanıcılara göre daha fazla kaldırabilecek durumdadır. Temel altyapıyı yapan deneyimli geliştiricilere bu işi bırakmak doğru yöndür
    • Rust için wasm VM’ler arasında şeffaf M:N zamanlamaya benzer bir şey sunan örnekler gördüm; böyle bir yaklaşım çoğu async zorluğunu çözebilir gibi. Nasıl gelişeceğini görmemiz gerekecek
  • Async Everything kötü bir dil
    async/await, JavaScript’te düzgün engelleyici (blocking) thread’ler olmaması sorununu düzeltmeye çalışan korkunç bir fikirdi ve şimdi her dile eklemleniyor. Dili ve kütüphane ekosistemini ikiye bölüp daha uzun süre acı yaratacak
    JavaScript dışında çoklu iş parçacığıyla çalışmış herkes, aktörlerin veya iletişim kuran sıralı süreçlerin çoklu iş parçacığı için en iyi yöntem olduğunu bilir
    Joe Armstrong’un tezinde de çok iş parçacıklı programları anlamanın tek yolunun her thread için katı biçimde sıralı kod yazmak ve birden çok thread’in kodunu tek bir yerde birbirine karıştırmamak olduğu anlatılıyor. Sorundaki gerçek eşzamanlı etkinliklerden biri, programlama dilindeki eşzamanlı süreçlerden birine tam olarak karşılık gelmeli ki kavramsal boşluk en aza insin: https://erlang.org/download/armstrong_thesis_2003.pdf
    Java’nın Project Loom’unu uygulayan Ron Pressler’ın async/await eleştirisi de iyi: https://www.youtube.com/watch?v=oNnITaBseYQ

    • JavaScript’ten nefret etmek eğlenceli ama Ryan Dahl’ın Node.js’yi ilk tanıttığı sunumu yeniden izlemek ilginç: https://www.youtube.com/watch?v=EeYvFl7li9E
      JavaScript’in kendisine karşı epey ikircikliydi; asıl hedefi epoll() G/Ç olay döngüsünü insanın gözünü oymak istemeyeceği bir soyutlamayla ele almanın yolunu bulmaktı. Ondan önce başka birçok yöntemi de denemişti
    • async/await aslında JavaScript’te değil, C#’ta başladı
      C#’tan Anders Hejlsberg TypeScript’i de yaptı; TypeScript’teki sınıflar, ok fonksiyonları, async/await gibi özellikler sonunda ES6+’a girdi
      Tek iş parçacıklı olay döngüsüne sahip JS/TS için harika bir çözüm olduğunu düşünüyorum. Ama dil ne kadar düşük seviyeliyse bir soyutlama olarak o kadar kötüleşiyor; bu yüzden buradan çıkan async Rust eleştirilerinin çoğu yerinde
  • Yazı, async Rust’ın karmaşıklığını ve zorluğunu iyi açıklıyor; ancak Rust’ın temel felsefelerinden birinin performanstan ödün vermeden bellek güvenliği olduğunu da unutmamak önemli
    Rust’ın asenkron kalıpları, özellikle derleyicinin veri güvenliğini garanti etmesini sağlama biçimi, bu felsefeyi iyi gösteriyor. Karmaşıklık var, ama geliştiriciyi veri ve yürütme akışı üzerine derin düşünmeye iten daha güvenli bir eşzamanlılık modeli olma değeri de var
    Rust tüm büyük ölçekli eşzamanlı kullanıcı alanı uygulamaları için doğru yanıt olmayabilir; fakat sağlamlık ve güvenliğin en öncelikli olduğu sistemlerde bu ödünleşim haklı görülebilir. Ekosistem geliştikçe bu acıları azaltacak soyutlamaların ve kütüphanelerin daha da ortaya çıkması muhtemel

  • async tabanlı, kilitsiz Rust’ı çok yazıyorum. Başlıca sorun Tokio future’larının 'static olması; bu da Rust ekosisteminin derinlerine işlemiş bir tasarım hatasından, yani bellek sızıntısının güvenli olduğu kararından kaynaklanıyor
    Bu yüzden future’ın düzgün temizleneceğini statik olarak garanti edemiyorsunuz. Bir asenkron görev oluşturulduğunda biri std::mem::forget ile future’ı unutursa, ödünç alma denetleyicisi o future’ın geçişli olarak aktardığı referansların hâlâ canlı olduğunu bilemez
    Arc’ı her yere serpiştirmek yerine şu unsafe crate’i kullanıyorum: https://docs.rs/async-scoped/latest/async_scoped/
    Bu kadarı C++’ta yapacağım hataların %99’unu yakaladığı için makul bir ödünleşim. Güvenli biçimde 'static olmayan future uygulama çalışması da sürüyor; umarım başarılı olur
    Bir diğer büyük sorun da async traitin şu anda kutulanmış future gerektirmesi ve bunun her fonksiyon çağrısı sınırında malloc/free eklemesi; bu yılki düzeltme yol haritasında
    “Sadece kanal kullanın” tavsiyesi de büyük kod tabanlarında kontrol akışını her yana dağıtıyor. Kanallar modern bir GOTO gibi hissettiriyor; ben de kullanıyorum ama sadece birkaç işi paralel çalıştırıp sonra tamamlanmalarını beklediğim durumlarda pek kullanmıyorum

    • Önemli ayrım şu: Tokio future’larının kendisi 'static değil; çalışma zamanının eşzamanlılığından yararlanarak spawn edilebilen future’ların yalnızca 'static olması
      Future’ın poll() edilebilmesi için Pinlenmesi gerekir ve Pinlenmiş T: !Unpin eninde sonunda Drop çağırmak zorundadır: https://doc.rust-lang.org/std/pin/#drop-guarantee
      Derleyicinin async özelliğiyle üretilen future’lar bu niteliğe sahiptir; elle yazılmış future’lara da PhantomPinned eklenebilir. Bu sayede poll() edildikten sonra mem::forget ile oynanmasını tanımsız davranış olarak varsaymak mümkün olur; intrusive ve kendine referanslı future kütüphaneleri de mümkün hale gelir: https://docs.rs/futures-intrusive/latest/futures_intrusive/
      Future, Arc/Rc tarafından canlı tutulup sızdırılabilir; ama kütüphane geliştiricisi açısından bu, normal kullanımdan makul biçimde ayırt edilemez ya da çok da önemsenmesi gerekmez
    • Bellek sızıntısının güvenli olmasını bir tasarım hatası olarak görüyorsanız, iç değiştirilebilirliği kaldırmayı mı, Rc’yi kaldırmayı mı, yoksa bulaşıcı unsafe trait sınırları eklemeyi mi tercih ederdiniz, merak ediyorum