1 puan yazan GN⁺ 2023-10-08 | 1 yorum | WhatsApp'ta paylaş
  • Rust async runtime tartışması, “çekirdek başına OS thread” olup olmamasından çok, yükü iş çalma (work-stealing) ile mi dengeleyeceğiniz yoksa veri taşımayı azaltmak için share-nothing mi seçeceğiniz meselesine daha yakın
  • async Rust, kullanıcı alanında eşzamanlılık işleme ve asenkron I/O gibi thread-per-core’un temel koşullarını zaten karşılıyor; geriye kalan tartışma çekirdekler arası durum paylaşımından ne kadar kaçınılabileceği
  • İş çalma, thread’lerin boşta kalma süresini azaltarak kuyruk gecikmesini (tail latency) ve CPU kullanımını iyileştirmeye çalışır; ancak Rust’ta yield noktalarını aşan durumun thread-safe olması gerektiğinden Send kısıtı beraberinde gelir
  • share-nothing, veriyi belirli bir çekirdeğin önbelleğinde tutarak gecikmeyi düşürebilir; ancak anahtar-değer deposu gibi durum bölmenin kolay olduğu durumlarla, transaction/atomik değişiklik gerektiren durumların zorluk seviyesi oldukça farklıdır
  • Enberg makalesi share-nothing ile shared-state’i karşılaştırır; fakat iki taraf da iş çalma kullanmadığından, bu sonucu doğrudan Rust’ın iş çalan yürütücülerine yönelik bir eleştiriye bağlamak zordur

Rust async runtime tartışmasının özü

  • Rust topluluğunda, tokio gibi başlıca async runtime’ların varsayılan olarak çok iş parçacıklı yürütücü kullanması ve birden çok task’ı iş çalma ile dinamik olarak dengelemesi üzerine tartışma sürüyor
  • Bazı kullanıcılar, bu varsayılan nedeniyle Send + 'static veya Send + Sync + 'static kısıtlarının kod yazma deneyimini bozduğunu eleştiriyor
  • Tek thread’li sunucuları tercih edenler de var; ancak burada Rust sisteminin bir veya daha fazla CPU çekirdeği zamanı kullanmak istediği durum varsayılıyor
  • Alternatif olarak öne sürülen “thread-per-core”un daha hızlı ve uygulamasının da daha kolay olması bekleniyor; ancak performans ile uygulama kolaylığını aynı anda elde etmek zor

“thread-per-core” adının yarattığı kafa karışıklığı

  • Mevcut çok iş parçacıklı async yürütücüler de geniş anlamda thread-per-core kapsamına girer
    • Çekirdek başına OS thread oluştururlar
    • Çekirdek sayısından çok daha fazla task’ı bu thread’lerin üzerinde zamanlarlar
  • Pekka Enberg, thread-per-core’u üç fikrin birleşimi olarak özetler
    • Pahalı kernel thread’leri yerine eşzamanlılığı kullanıcı alanında işler
    • Çekirdek başına thread’lerin bloklanmaması için asenkron I/O kullanır
    • Senkronizasyon maliyetini ve CPU önbellekleri arasındaki veri hareketini ortadan kaldırmak için veriyi CPU çekirdekleri arasında böler
  • Enberg, yüksek throughput’lu sistemler oluşturmak için ilk iki maddenin önemli olduğunu, üçüncüsünün ise yalnızca çok büyük çok çekirdekli makinelerde gerekli olabileceğini düşünür
  • async Rust kullanıyorsanız, kullanıcı alanı eşzamanlılığı ve asenkron I/O şeklindeki ilk iki koşul zaten karşılanır
  • Dolayısıyla tartışmanın merkezi thread-per-core’un kendisi değil, iş çalma ile share-nothing arasında hangi optimizasyonun seçileceğidir

İş çalmanın amacı ve maliyeti

  • İş çalma, tüm thread’lerin sürekli yapacak işi olmasını sağlayarak kuyruk gecikmesini azaltmaya yönelik bir optimizasyondur
  • Gerçek sistemlerde her task’ın gerektirdiği iş miktarı farklıdır
    • Bir HTTP isteği başka bir istekten çok daha fazla iş gerektirebilir
    • Başta thread başına işleri dengeli dağıtsanız bile, öngörülemeyen task farklılıkları nedeniyle zamanla iş miktarı değişebilir
  • Maksimum yükte bazı thread’ler işleyebileceklerinden fazla iş alırken, diğer thread’ler boşta kalabilir
  • tokio, async-std ve smol; kuyruk gecikmesini azaltmayı ve CPU kullanımını iyileştirmeyi hedefleyerek iş çalma uygular
  • Maliyet, bir task’ın bir thread’de durduktan sonra başka bir thread’de yeniden başlayabilmesidir
    • yield noktalarını aşarak kullanılan durum thread-safe olmalıdır
    • Rust API’lerinde bu, future’ın Send olması gereken bir biçimde görünür
    • Sistemin durumunu net kavrayamıyorsanız Sendin nasıl garanti edileceğine karar vermek zordur
  • Durum başka bir thread’e taşındığında senkronizasyon maliyeti ve önbellek kaçırmaları oluşur; bu da her CPU’nun yalnızca kendi durumuyla ilgilendiği share-nothing ilkesiyle çelişir

share-nothing’in performans mantığı

  • share-nothing, veriyi birden fazla çekirdeğin paylaştığı yavaş önbellekte değil, belirli bir CPU çekirdeğinin daha hızlı önbelleğinde tutarak kuyruk gecikmesini azaltmayı hedefleyen bir tasarımdır
  • Enberg’in The Impact of Thread-Per-Core Architecture on Application Tail Latency makalesi, share-nothing anahtar-değer deposu ile shared-state yapısındaki memcached’i benchmark ederek kuyruk gecikmesinde iyileşme gösterir
  • Rust topluluğunda bu makalenin “%71 performans artışı” gibi basitçe alıntılanma biçimi yüzeysel ve faydasızdır
  • Enberg’in anahtar-değer deposu, share-nothing yapı için durum ve bağlantıları böler
    • Bir hash fonksiyonuyla keyspace’i birden çok thread’e böler
    • Gelen TCP bağlantılarını thread’ler arasında bölmek için SO_REUSEPORT kullanır
    • İsteği, bağlantıyı yöneten thread’den ilgili keyspace’i yöneten thread’e mesaj iletimi kanalıyla yönlendirir
  • memcached’de tüm thread’ler bölünmüş keyspace’in sahipliğini paylaşır ve her partition bir mutex ile korunur
  • Enberg’in sonuçları, mutex yerine kanal kullanan bir yapının daha düşük kuyruk gecikmesine ulaşabileceğini gösterir
    • Aynı partition’a tekrar tekrar erişildiği ve bir çekirdeğin önbelleğinde kaldığı için önbellek kaçırmalarının azaldığı düşünülebilir
  • Ancak gelişmiş kernel özellikleri ve dikkatle planlanmış bir yapıyla veri hareketinden kaçınan tasarımın, veriyi mutex içine sarmaktan daha kolay uygulanacağını söylemek zordur

Durum bölmenin kolay ve zor olduğu durumlar

  • Anahtar-değer depoları, uygulama durumunu birden çok thread’e bölmeyi kolaylaştırdığı için share-nothinge iyi uyan bir örnektir
  • Daha karmaşık uygulamalarda birden çok partition’ın durumunu transaction şeklinde veya atomik olarak değiştirmek gerekiyorsa, doğru uygulama için çok daha fazla özen gerekir
  • share-nothing savunusu, geçmişte eventual consistency veritabanları etrafındaki aşırı heyecanla benzer yönler taşır
    • Performans iyileşebilir
    • Buna karşılık veri tutarsızlığından doğan hatalardan kaçınmak için dikkatli tasarım gerekir

Enberg makalesi ile Rust iş çalma tartışması arasındaki mesafe

  • Enberg’in implementasyonu ve memcached’in ikisi de iş çalma kullanmaz
  • Bu nedenle makalenin temel performans sonucunu Rust’ın iş çalma mimarisiyle doğrudan ilişkilendirmek zordur
  • Enberg’in yapısına iş çalma eklemek veri hareketini bir miktar artırabilir; ancak CPU kullanımını yükselten bir yöntem olma ihtimali de vardır
  • memcached’e iş çalma eklenmesi durumunda bunun neden yardımcı olmayacağını hayal etmek zor görünüyor
  • Makaledeki implementasyon, keyspace’in dengeli bölünmesi ve SO_REUSEPORT ile işi baştan eşit dağıtacak şekilde tasarlanmıştır
  • Gerçek ortamlarda dinamik dengesizlik ortaya çıkabilir
    • hot key, daha fazla okuma ve yazma alarak ilgili keyspace’i yöneten thread’in iş miktarını artırır
    • Bazı bağlantılar diğerlerinden daha fazla istek gerçekleştirerek ilgili bağlantıyı yöneten thread’in iş miktarını büyütür
  • Makalenin benchmark’ının, her bağlantının rastgele anahtarlar üzerinde sabit bir iş miktarı gerçekleştirdiği ve bu dengesizlik koşullarını yeniden üretmediği anlaşılıyor
  • hot key’i ek partition’larda önbelleğe almak gibi, share-nothing sistem içinde de dengesizliği hafifleten tasarımlar mümkündür
  • Bazı task’lar durum hareketini önlemek için belirli bir çekirdeğe sabitlenirken, başka iş çalma biçimleri optimizasyon olarak kullanılabilir

Pratik sonuç

  • Sistemi CPU önbellekleri arasında veri hareketinden kaçınacak şekilde dikkatle tasarlarsanız, bunu yapmadığınız duruma göre daha iyi performans elde edebilirsiniz
  • Ancak en büyük şikâyetiniz Send kısıtını generic’lere eklemekse, bu tür ince ayarlı performans mühendisliği yaptığınız pek söylenemez
  • Zaten shared state kullanacak bir sistemse, yük altında iş çalmanın CPU kullanımını iyileştirmeyeceğini söylemek zordur

1 yorum

 
GN⁺ 2023-10-08
Hacker News yorumları
  • Şahsen bu yazı bana büyük resmi kaçırıp ayrıntılara takılıyor gibi geliyor
    Tartışmanın özü çekirdek başına iş parçacıklı, iş çalma kullanan bir yürütücü değil; Rust’ta async/await’in bunun için iyi bir soyutlama olup olmadığı
    Async kodu ne kadar çok kullanırsam, soyutlamanın o kadar sızdırdığını ve ona karşı programlamanın zor olduğunu hissediyorum
    İnsanların istediği alternatif eşzamanlılık modeli, iş çalma kullanan bir yürütücünün üzerinde yığınlı coroutine’ler ve kanallar üzerinden yapılandırılmış eşzamanlılık
    Birisi bunu uygulayıp async/await ve future tabanlı modelle karşılaştırana kadar verimli bir tartışma yapmak zor; async’ten hoşlanmayanlar ondan kaçınacak, Send + Sync + 'static ile uğraşmayanlar ise kullanmaya devam edecek

    • O halde gerçekten C10M ölçeğine ihtiyaç yoksa, bağlantı başına bir iş parçacığı kullanıp daha karmaşık şeylere girmemek yeterli değil mi diye düşünüyorum
      Böyle bir ölçeğe ihtiyaç duyulan işler çoğu durumda oldukça nadir olsa gerek
      Bu tür yazılar “çekirdek iş parçacıkları pahalıdır” deyip bunun özünde doğru olduğu varsayımıyla geçiyor, ama gerçekte öyle değil
      İş sürekli hiçbir iş yapmayan görevler üretmekten ibaret değilse “gerçek iş parçacıklarının” ek yükü muhtemelen düşüktür; karşılığında elde edilen sadelik ise muazzamdır
    • async/await ile yığınlı coroutine tartışmasını daha ilginç buluyorum, ama bu yazıdaki tartışma o değil
      withoutboats’un ele aldığı alıntı, bağlantılı yazıda varsayılan çok iş parçacıklılığı ve iş çalmayı özellikle eleştiren bölüm
      [1] https://www.reddit.com/r/rust/comments/16p47f1/the_state_of_...
    • Dünyada insanların aynı fikirde olmadığı birden fazla konu var
      Bu yazı benim yazılmasını istediğim tartışmayı değil, yalnızca başka bir tartışmayı ele alıyor
    • async/await’i kanallarla birlikte kullanırken, async fonksiyon argümanları olarak yalnızca değişmez referanslar veya kopyalanabilir tipler geçirip, değiştirilebilir paylaşımlı tiplerin kanallar üzerinden iletişim kurmasını zorunlu kılabilirsiniz
      Değiştirilebilir bir tipi sahiplenen ve kanallarla iletişim kuran Erlang tarzı bir sunucu da oluşturabilirsiniz; Arc ile devam etmeyi de seçebilirsiniz
      Rust ikisini de yapabilecek gücü verir
    • Benim anladığım kadarıyla Rust’ın mevcut async desteği üzerinde kanal kullanmayı engelleyen bir şey yok
      Yığınlı coroutine’ler bu noktada pek anlamlı görünmüyor, çünkü o durumda zaten ayrı iş parçacıkları kullanabilirsiniz
  • Yaklaşık 15 yıl önce çekirdek başına iş parçacığı modelinin ilk çözmeye çalıştığı asıl sorun, genel amaçlı çok çekirdekli sunucularda hesaplama ölçeklenebilirliği ve verimlilik elde etmekti
    Bazı iddiaların aksine, çekirdek başına iş parçacığı açıkça CPU merkezli iş yüklerini optimize etmek içindi; daha sonra daha gelişmiş G/Ç işleme gerekmiş olsa da, yüksek verimli G/Ç merkezli işlerde de mükemmel olduğu ortaya çıktı
    Bu tür yazıları görünce, çekirdek başına iş parçacığı mimarisi benimsenirken yapılan birçok yazılım tasarımı hatasının hızla tekrarlandığı izlenimine kapılıyorum
    Adil olmak gerekirse, çekirdek başına iş parçacığıyla ilgili bilgisayar bilimi ağırlıklı olarak HPC’den geldiği için yeterince belgelenmiş değil
    Bu yazı, çekirdek başına iş parçacığı mimarisinin zor bir sorununa, yani çekirdekler arası iş dengelemeye odaklanıyor
    Temel model veri/yük için push/pull olmak üzere dört olasılıktan oluşur; iş çalma özünde bir yük çekme (load pull) modelidir
    Bu yaklaşımın ek yükü yalnızca neredeyse hiç kullanılmasına gerek kalmadığında, yani gerçek problemlerde nadir görülen doğal bir denge bulunduğunda düşüktür
    Çekirdekler arasında dinamik yük kaymasının yaygın olduğu daha ilginç iş yüklerinde, koordinasyon ek yükü nedeniyle iş çalma performans darboğazına dönüşür
    Yine de anlaşılması kolay olduğu için uygun iş yüklerinde hâlâ kullanılır, ancak genellemesi pek iyi değildir
    Yazıda değinilmeyen nadir iş yüklerinden bazılarında muhtemelen en iyi seçenek de olabilir
    Günümüzde en çok güç kazanan model veri itme (data push) gibi görünüyor; daha az sezgisel olsa da iş parçacıkları arasında çok daha az koordinasyon gerektiriyor
    Bu modelin de uygun olmadığı iş yükleri var, ancak yaygın iş yüklerinin çoğuna iyi genelleniyor
    Çekirdek başına iş parçacığı mimarisi varlığını sürdürecek. Ölçeklenebilirlik ve verimlilikte onunla baş etmek mümkün değil
    Ancak modern ve deyimsel bir çekirdek başına iş parçacığı tasarımının nasıl göründüğüne dair çoğu yazılım mühendisinde sezgi eksikliği var; bu konuyu derinlemesine ele alan yazı veya makalelerin azlığı da durumu daha kötüleştiriyor

    • Rust async framework’lerinin iş çalmayı kullanmasının başlıca nedeni, framework düzeyinde etkinleştirmesinin kolay olması ve birçok uygulamada, özellikle de ideal şekilde tasarlanmamış uygulamalarda performansı iyileştirmesi gibi görünüyor
      Profildeki biyografi ve bu açıklamaya bakınca, bu tür uygulamalar muhtemelen sizin esas uğraştığınız tür değil
      İlgili literatüre bağlantılar varsa görmek isterim
    • Veri itme fikrinin ne olduğunu merak ediyorum
  • “I/O ağırlıklı demek, Rust ile yazıldığında tek bir çekirdeği doyuracak kadar iş olmadığı anlamına gelir. Öyleyse doğal olarak tek iş parçacıklı bir sistem yazın” cümlesine katılıyorum
    Kullandığım uygulamaların önemli bir kısmı arka planda olaylara tepki veren daemon’lar ve bunları tek iş parçacıklı yapınca Arc ve Mutex ek yükünü ortadan kaldırabiliyorum
    Bu ek yük o noktada çoğunlukla sözdizimsel bir külfete yakın olsa da, hata ayıklamayı ve bakımı kolaylaştırıyor
    Yalnızca ihtiyaç duyduğun şeyin bedelini ödeme fikri Rust’ta sevdiğim taraflardan biri
    Bu yazının yanıt verdiği asıl yazı, tokio ve diğer async kütüphanelerin basit tek iş parçacıklı mimariye geri dönmeyi zorlaştırdığını eleştiriyor
    Biraz abartı var ama genel olarak bu eleştiriye katılıyorum
    Yüksek iş hacimli uygulamalar için daha iyi diye varsayılanları daha karmaşık yapmak, Rust’ın ideallerinin tersine gibi görünüyor

    • Böyle servisler yazdım ama onlara I/O ağırlıklı demezdim
      İş hacmine bağlı değiller; çoğunlukla boşta durup bir iş çıkınca sistem kaynak kullanımını azaltmak için hızlıca bitirmeye çalışan yapılar
      Ara sıra devasa bir iş patlaması olmuyor ve o anda gecikme süresi çok kritik değilse, birden fazla iş parçacığı kullanmak fayda sağlamadan yalnızca karmaşıklık ve ek yük ekler
    • Doğru. Eşzamanlılık isteyen her şey web sunucusu değil
      İşletim sistemlerinde tüm sistem servisleri IPC isteklerini eşzamanlı işlemek zorundadır, ama çoğu toplam CPU tüketimini azaltmak için bunu tek iş parçacığında yapar
      4 çekirdekli bir cihazda onlarca servisi çekirdek başına iş parçacığı şeklinde yapmak CPU ve RAM israfıdır
    • tokio, gerçekten gerektiğinde kullanılabilecek tek iş parçacıklı yürütücü desteği sunuyor ve zor da değil
      tokio API’sinde buna LocalSet deniyor
      https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...
    • Aynı alıntı hakkında yazmak üzereydim
      Tek bir CPU’daki tek bir iş parçacığı yeterli olsa bile yine de eşzamanlılık isteyebilirsiniz
    • Arc ve Mutex yerine Rc ve RefCell kullanacaksınız; kod açısından benzer ölçüde karmaşık ve laf kalabalığına yol açmıyor mu diye düşünüyorum
      Daha az verimli olduğunu anlıyorum, ama anlattığınız durumda birkaç atomik işlemin ek maliyeti zaten ihmal edilebilir olmaz mı?
  • Alıntı hakkında konuşacak olursak, “Rust async programlamanın ilk günahı varsayılanı çok iş parçacıklı yapmaktır… Send + 'static, daha da kötüsü Send + Sync + 'static, Rust kullanmanın keyfini öldürüyor” ifadesinin fazla melodramatik olduğuna katılıyorum
    Send + Sync kaldırılınca o kadar büyük bir fark olacağını sanmıyorum
    En rahatsız edici olan 'static ve bunun nedeni iş çalma değil
    İstediğim şey kapsamlı eşzamanlılık
    Örneğin <https://github.com/tokio-rs/tokio/issues/2596> gibi bir şey
    Şu anda Rust async’te gerçekten nefret ettiğim bir diğer şey de gözlemlenebilirlik eksikliği
    Şirkette bazı task’ların öylece durup kaldığı bir production sorunumuz var ve gdb; thread apply all bt muadilini yapmak istiyorum
    En azından <https://github.com/tokio-rs/tokio/issues/5638>’in gelmesini bekliyorum
    Şu anda deneysel olarak mevcut ama deneyimime göre bazen panic üretiyor
    Bugün gerçekten bir PR yazıp SIGTERM sırasında deneysel sürümü denemeye çalışacağım
    Zaten kapanış sırasında olduğumuz için çökse de sorun değil diye değerlendiriyorum
    Bu şikâyetlerin hiçbiri iş çalmayı kaldırmakla çözülmez
    Daha fazla madde saysam da iş çalmayı kaldırmak neredeyse hiçbir şeye yardımcı olmaz

    • Java çok eleştiri alıyor ama bu kısım yaklaşık 25 yıldır gayet iyi çalışıyor ve sorun çözmede inanılmaz yararlıydı
      Debugger’a bile gerek yok; JVM’e SIGQUIT gönderince tüm iş parçacıklarının stack trace’lerini stderr’e döker ve çalışmaya devam eder
      Her iş parçacığının hangi lock’u tuttuğu veya beklediği de buna dahildir
      Başka diller kullandığım her seferinde bu özelliği özlüyorum
      Production’da geçici profiling için de kullanılabilir
      Birkaç kez snapshot alıp grep/sed/sort/uniq ile hot spot’ları bulmak yeterli
    • Bu tür sorunları hafifletmek için şirkette hangi task’ın çalıştığını ve her task’ın o anda hangi dosya/satır numarasında olduğunu periyodik olarak loglayan bir kütüphane kullanıyoruz
      Her await noktasının önüne elle r.set_location(file!(), line!()); serpiştirmek gerekiyor, ama sistemin neden durmuş gibi göründüğünü açıklamada birçok kez yardımcı oldu
      [1] https://github.com/antialize/tokio-tasks/blob/main/src/run_t... içinde set_location() var, task.rs içinde de list_tasks() var
  • Bu sorunun doğru bir cevabı yok; tamamen kullanım senaryosuna bağlı
    Sonuçta mesele, I/O ağırlıklı ve CPU ağırlıklı iş yükleri ile önbellek tahliyesi ya da kilit çekişmesi gibi şeylerin ne kadar kötü etki yarattığı
    Harici bir veritabanıyla konuşan ve arada hafif iş mantığı bulunan bir HTTP sunucusunu paylaşımlı bir sanal sunucuya koyuyorsanız, iş çalma ve thread yeniden kullanımı sezgisel olarak mantıklı geliyor
    Elbette her zaman benchmark yapmak gerekir
    Öte yandan bir veritabanı ya da benzeri bir sistem geliştiriyorsanız ve yük altında yüksek eşzamanlılık ile çok sayıda bağlam değişimi her yerde önbellek tahliyesi ve çekişme yaratıyorsa, işiniz zorlaşır
    Bu durumda çekirdek başına thread oldukça makuldür; async framework'ün kendisi bile anlamsız olabilir
    Neyin “daha iyi” olduğuna dair dogmatik bir doğru cevap yok
    Uygulamayı profillemeniz gerekir
    Daha önce de söyledim; Rust'ın genel odağının web servisi geliştirme alanındaki büyük akınla çarpıtıldığını hissediyorum
    Rust'ın bu işler için uygun dil olup olmadığından hâlâ emin değilim, ama onlarda oldukça iyi çalışıyor gibi göründüğüne göre buna diyecek bir şey yok
    Ancak dil hakkındaki açık tartışmalar ve öne itilen crate'ler şu anda genel olarak bu önyargıyı yansıtıyor
    Bu aynı zamanda bu forumdaki pek çok yazılım mühendisinin önyargısı

    • async'in Rust'ın diğer kısımlarını etkilediğini pek görmedim
      Oyun motorları, kriptografi kütüphaneleri, çekirdekler, komut satırı araçları, derleyiciler gibi sistem uygulamaları async'e dokunmadan da başarıyla geliştiriliyor
      Büyük bir kriptografi kütüphanesinin bakımını yapıyorum ve async tarafındaki işlerden hiç etkilenmedim
    • Web geliştiricilerinin akınına katılıyorum
      Bunun sonucunda Rust'ın sistem programlama açısından kullanışlılığı bozulursa üzücü olur
    • Web uygulamalarının aslında çekirdek başına thread için ideal olduğunu düşünmüştüm
      Uygulamanın kendisinde isteklerin dışında neredeyse hiç durum yok; soket listener'ları ve veritabanı bağlantıları thread başına bölünebilir
      Kalan durumun da istekler arasında büyük ölçüde statik olma ihtimali yüksek, bu yüzden önbellek geçersizleştirme sık yaşanmaz
      Paylaşılan durum olmadığından sahiplik yönetimi de kolay olmalı
    • Rust topluluğunun pazar ve bilinirlik kazanmak için bu geliştiricileri oldukça aktif biçimde çektiğini düşünüyorum; bu yüzden bunlar yaşanıyor
      Bunun iyi ya da kötü olduğunu söylemeye çalışmıyorum, ama artık Rust kalitesi değişken web odaklı kütüphane ve framework'lerin bitmek bilmeyen akışıyla yaşamak zorunda
      Ayrıca temel altyapı kütüphanelerinin ve crate'lerin önemli bir kısmı async öncelikli bir yaklaşım benimsediği için async tartışma konusu olmaya devam edecek
      Artık sıradan bir geliştiricinin, projenin ana ilkesi olarak async kullanmayacağını söylemediği sürece, iş problemleri için olağan senkron kod yazması zorlaşmış durumda
  • Executor thread'leri arasında task taşınmasına izin vermek için Send sınırı istemek, Rust async sisteminin kendisinde açık bir kusur gibi görünüyor
    Çünkü async Drop'un temel sorunuyla birlikte kapsamlı API'ler uygulanmasını engelliyor
    Thread'lerde olduğu gibi, Send sınırının yalnızca spawn ya da kanallar üzerinden veri gönderme gibi fonksiyonlarda bulunması yeterli olmalı
    Paylaşmayan yaklaşım çoğu zaman bu kusuru gizlemeye yönelik bir geçici çözümden fazlası değil
    Task'ları belirli bir thread'e/çekirdeğe isteğe bağlı olarak sabitlemenin avantajları var ve bazı durumlarda gerçekten faydalı, ama bu daha ince bir tartışma; Send ile ilgili async kullanıcı şikâyetleriyle pek ilgisi yok

  • Güzel bir yazı; sadece başlığa bakmayıp devamını okumanızı öneririm
    En sevdiğim cümle şuydu: “En büyük şikâyetleri bir generic'e Send sınırı eklemekse, o kişinin bu seviyede mühendislik yaptığına inanmak zor”
    Düzenleme: “duped”ın yorumuna tamamen katılıyorum
    Bu tartışmanın daha geniş bağlamını bilmediğim için bu cümleyi fazla aceleyle alıntılamış olabilirim

  • “Sistem durumunu doğru göremeyen kişiler, future'ın Send olmasını garanti etmenin en iyi yolunu bulmakta zorlanabilir” kısmı biraz kibirli gelmiyor mu?
    'static yaşam süresi ve Send/Sync kısıtlarıyla ilgili sorunlar geliştiriciler arasında yaygın biçimde karşılık buluyor; onların aptal olduğu izlenimini almadım

    • Önce söylediğime yeniden işaret ediyorum
      İnsanlar iş çalma yapmamanın daha kolay ve daha hızlı olduğunu söylüyor
      Benim iddiam bunun ikisinden biri olduğu
      Paylaşmayan yöntemin daha hızlı olması için, kodu paylaşımlı durum mimarisini thread-safe yapmaktan daha kolay olmayan bir şekilde tasarlamanız gerekir
      Sonraki paragrafta “yavaş”a karşılık gelen paralel bir cümle var
      Paralel ve eşzamanlı Rust'ı derlenir hâle getirmekte zorlanan insanların aptal olduğunu düşünmüyorum
      Sadece, bizim sunduğumuz API hayatlarını mahvetmiş gibi davranılmasından hoşlanmıyorum
    • “Zor”un tırnak içinde olmasının sebebi, bunun mutlaka daha zor olmaması
      Zaten yapmanız gereken bir şeyse daha zor değildir
      Mesele “insanların önemsiz bir işe aşırı tepki vermesi”nden çok, async'in eninde sonunda çözmeniz gereken sorunu daha erken çözdürmesi
      Borrow checker'a benzer bir his
      Bazen aşırı kısıtlayıcıdır, ama bazen de her şeyin yolunda olduğunu varsaydığınızda hesaba katmadığınız uç durumlar gerçekten vardır
    • Kişisel olarak ben de durumumun Send olup olmadığını anlamakta zaman zaman kesinlikle zorlanıyorum
      Bu yüzden alıntılanan cümle bana dokunuyor
  • Bu yazı büyük resmi kaçırıp ayrıntılara takılıyor
    Her programda en iyi performansı almanın her zaman doğru olan bir yolu yok
    İstediğiniz kadar tartışabilirsiniz ama çekirdek başına thread'in artıları ve eksileri klasik bir “duruma bağlı” meselesi
    Sorun, en başta async kullanımının kendisinin erken optimizasyon olmasında
    Rust programlarının %99'u redis ya da linkerd gibi şeyler değil
    Python ya da Ruby ile yazılsa da yeterince hızlı olacak CLI araçları veya web uygulamaları
    O hâlde topluluk neden bloklayan I/O kullanan Rust'ı terk edip her şeyi async yaptı; geliştiriciler neden varsayılanmış gibi her şeyin başına #[tokio::main] koyuyor diye düşünüyorum

    • Bunun bir nedeni, çekirdek başına thread performansı yeterliyse en başta Rust kullanmak istememe ihtimalinizin yüksek olması olabilir
      Hızdan feragat edip programlama deneyimi daha iyi olan diller var; Python buna bir örnek
      Rust kullanmak istiyorsanız muhtemelen ek performansa ihtiyacınız vardır ve zaten daha az rahat bir dili kabul ettiğiniz için, daha iyi performans uğruna daha az rahat bir stili de kabul edebilirsiniz
  • Rust kullanmışlığım yok ama şikâyeti anlıyorum.
    Gerekmeyen ve CPU kapasitesinin bol olduğu durumlarda, tek bir isteğin uçtan uca gecikmesini aksine artırabilecek yük yeniden dağıtımı uğruna, durumun thread’ler arasında taşınabilmesi için kodu özel bir şekilde yazmak zorunda kalmak sinir bozucu olurdu.
    Taşınabilir durumun varsayılan olduğu ve neredeyse hiç bozulmadığı bir platformda bu yaklaşım mantıklı olabilir; ama Rust’ta durum böyle değil gibi.
    Merak ettiğim şey kullanım hissi.
    Bunun daha çok “Send denen sihirli büyüyü eklemezsen kod derlenmez” durumuna mı, yoksa “iş çalma sırasında durum bozulur ve aralıklı, debug etmesi zor biçimlerde hata verir” durumuna mı yakın olduğunu merak ediyorum.

    • Kabaca şöyle bir kod yazdığını varsayalım.
      Bir Server var ve serve içinde mesajları okuyup her mesaj handler’ını yeni bir task olarak spawn(async move { ... }) ediyorsun.
      Başta her şey düzgün çalışır.
      Sonra bir gün do_this implementasyonunu değiştirirsin ve this’in tipi artık Send olmaktan çıkar; bunun üzerine spawn(...), anonim async move { } scope’unun oluşturduğu tip Send olmadığı için korkutucu bir derleme hatası verir.
      Sebep her zaman açık değildir, hata mesajı da pek yardımcı olmaz.
      this Send değilse, do_that(arg).await içindeki .await boyunca elde tutulamaz.
      Çünkü her .await, future’ın yield edip executor tarafından başka bir thread’e schedule edilebileceği bir yürütme noktasıdır.
      Tipi Send yapabiliyorsan sorun yok.
      Ama bunu yapamadığın durumlar da gayet olabilir; o zaman future scheduling’i spawn_local gibi bir şeye çevirmen gerekir.
      Bunu çağırmak için epey boilerplate eklemen gerekebilir.
      Send meselesi budur.
      Sadece tip anotasyonu ekleme meselesi değil; bir tipin Send implement edip etmediği her zaman açık olmadığı için koda ince ince sızabilir ve daha sonra belirsiz biçimde kırılabilir.
    • Genel olarak derleyici Send ve Sync olup olmadığını algılar.
      Programı thread-safe yazarsan sorun çıkmaz.
      Esas nokta şu:
      İnsanlar Tokio async’in her yerde Send ve Sync istemesinden dolayı zor olduğundan şikâyet ediyor; ama aslında güvenli eşzamanlı kod yazmak, türü ne olursa olsun, başlı başına zor.
      Sezgisel değil; sorun da async’in bunu otomatik olarak “hallediyormuş” gibi hissettirmesi.
      Ama gerçekte öyle değil.
      Ne yaptığını bilmen gerekir; derleyici burada sadece yardımcı olur.
      Çekirdek başına thread modeline geçersen bazı durumlarda Send ihtiyacını gizleyebilirsin, ama hepsinde değil.
      Uzun vadede de mimari açıdan dönüp seni ısırabilir.
    • Kod, gereken her yerde her şey Send/Sync değilse derlenmez.
      Yanılıyor olabilirim ama bunu başarmanın tembel yolu genelde paylaşılabilecek şeyleri Arc veya Mutex ile sarmalamaktır.
    • Çekirdek başına thread’i varsayılan yapmak karşısındaki argümanın basitçe kurulabileceğini düşünüyorum.
      CPU ağırlıklıysa iş çalma çoğu durumda daha iyi olacaktır.
      I/O ağırlıklıysa çekirdek başına thread daha iyi çalışabilir; ama zaten CPU kapasitesi bol olduğundan performans çok da önemli değildir.
      Bence iş çalma, dil API’sine koymak için daha iyi varsayılandır.