Çekirdek başına iş parçacığı (Thread-per-core)
(without.boats)- 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
Sendkı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 + 'staticveyaSend + Sync + 'statickı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
Sendolması 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_REUSEPORTkullanı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_REUSEPORTile 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
Sendkı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
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 + 'staticile uğraşmayanlar ise kullanmaya devam edecekBö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
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_...
Bu yazı benim yazılmasını istediğim tartışmayı değil, yalnızca başka bir tartışmayı ele alıyor
Değiştirilebilir bir tipi sahiplenen ve kanallarla iletişim kuran Erlang tarzı bir sunucu da oluşturabilirsiniz;
Arcile devam etmeyi de seçebilirsinizRust ikisini de yapabilecek gücü verir
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
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
“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
ArcveMutexek yükünü ortadan kaldırabiliyorumBu 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
İş 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
İş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 API’sinde buna
LocalSetdeniyorhttps://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...
Tek bir CPU’daki tek bir iş parçacığı yeterli olsa bile yine de eşzamanlılık isteyebilirsiniz
ArcveMutexyerineRcveRefCellkullanacaksınız; kod açısından benzer ölçüde karmaşık ve laf kalabalığına yol açmıyor mu diye düşünüyorumDaha 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ıyorumSend + Synckaldırılınca o kadar büyük bir fark olacağını sanmıyorumEn rahatsız edici olan
'staticve 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 btmuadilini yapmak istiyorumEn 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
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/uniqile hot spot’ları bulmak yeterliHer
awaitnoktasının önüne eller.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.rsiçinde delist_tasks()varBu 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ı
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
Bunun sonucunda Rust'ın sistem programlama açısından kullanışlılığı bozulursa üzücü olur
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ı
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
Sendsı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,
Sendsı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;
Sendile ilgili async kullanıcı şikâyetleriyle pek ilgisi yokGü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
Sendsı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
Sendolmasını garanti etmenin en iyi yolunu bulmakta zorlanabilir” kısmı biraz kibirli gelmiyor mu?'staticyaşam süresi veSend/Synckı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İ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
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
Sendolup olmadığını anlamakta zaman zaman kesinlikle zorlanıyorumBu 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
asynckullanımının kendisinin erken optimizasyon olmasındaRust 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üyorumHı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 “
Senddenen 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.Bir
Servervar veserveiçinde mesajları okuyup her mesaj handler’ını yeni bir task olarakspawn(async move { ... })ediyorsun.Başta her şey düzgün çalışır.
Sonra bir gün
do_thisimplementasyonunu değiştirirsin vethis’in tipi artıkSendolmaktan çıkar; bunun üzerinespawn(...), anonimasync move { }scope’unun oluşturduğu tipSendolmadığı için korkutucu bir derleme hatası verir.Sebep her zaman açık değildir, hata mesajı da pek yardımcı olmaz.
thisSenddeğilse,do_that(arg).awaitiçindeki.awaitboyunca 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
Sendyapabiliyorsan sorun yok.Ama bunu yapamadığın durumlar da gayet olabilir; o zaman future scheduling’i
spawn_localgibi bir şeye çevirmen gerekir.Bunu çağırmak için epey boilerplate eklemen gerekebilir.
Sendmeselesi budur.Sadece tip anotasyonu ekleme meselesi değil; bir tipin
Sendimplement edip etmediği her zaman açık olmadığı için koda ince ince sızabilir ve daha sonra belirsiz biçimde kırılabilir.SendveSyncolup olmadığını algılar.Programı thread-safe yazarsan sorun çıkmaz.
Esas nokta şu:
İnsanlar Tokio async’in her yerde
SendveSyncistemesinden 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
Sendihtiyacını gizleyebilirsin, ama hepsinde değil.Uzun vadede de mimari açıdan dönüp seni ısırabilir.
Send/Syncdeğilse derlenmez.Yanılıyor olabilirim ama bunu başarmanın tembel yolu genelde paylaşılabilecek şeyleri
ArcveyaMutexile sarmalamaktır.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.