Neden tüm işlevler varsayılan olarak eşzamansız olmamalı?


107

Async-bekliyoruz .net 4,5 deseni paradigması değişiyor. Neredeyse gerçek olamayacak kadar iyi.

Bazı IO ağırlıklı kodları zaman uyumsuz bekleme moduna geçiriyorum çünkü engelleme geçmişte kaldı.

Oldukça az insan async-await'i bir zombi istilasıyla karşılaştırıyor ve ben bunu oldukça doğru buldum. Zaman uyumsuz kod, diğer eşzamansız kodu sever (zaman uyumsuz bir işlevi beklemek için eşzamansız bir işleve ihtiyacınız vardır). Böylece giderek daha fazla işlev eşzamansız hale gelir ve bu, kod tabanınızda büyümeye devam eder.

İşlevleri eşzamansız olarak değiştirmek bir şekilde tekrarlayan ve hayal gücü olmayan bir iştir. Bildirime bir asyncanahtar kelime ekleyin, dönüş değerini kaydırın Task<>ve hemen hemen bitirdiniz. Bu, tüm sürecin ne kadar kolay olduğu oldukça rahatsız edici ve çok yakında bir metin değiştirici komut dosyası benim için "taşıma" nın çoğunu otomatikleştirecek.

Ve şimdi soru ... Tüm kodum yavaş yavaş eşzamansız hale geliyorsa, neden hepsini varsayılan olarak eşzamansız yapmıyorsunuz?

Sanırım en bariz sebep performanstı. Async-await'in bir ek yükü ve asenkron olması gerekmeyen, tercihen olmaması gereken bir kodu vardır. Ancak tek sorun performans ise, elbette bazı akıllı optimizasyonlar gerekli olmadığında ek yükü otomatik olarak kaldırabilir. "Hızlı yol" optimizasyonu hakkında bir şeyler okudum ve bana öyle geliyor ki, tek başına çoğunu halledebilir.

Belki bu, çöp toplayıcıların getirdiği paradigma değişikliğiyle karşılaştırılabilir. İlk GC günlerinde, kendi hafızanızı boşaltmak kesinlikle daha verimliydi. Ancak kitleler hala daha az verimli olabilecek daha güvenli, daha basit kodlar lehine otomatik toplamayı tercih ediyor (ve bu tartışmalı bir şekilde bile artık doğru değil). Belki de burada durum bu olmalı? Neden tüm işlevler eşzamansız olmamalı?


8
C # ekibine haritayı işaretleme kredisi verin. Yüzlerce yıl önce yapıldığı gibi, "ejderhalar burada yatar". Bir gemiyi donatabilir ve oraya gidebilirsiniz, büyük olasılıkla güneş parlarken ve arkanızdaki rüzgarla hayatta kalacaksınız. Ve bazen değil, asla geri dönmediler. Zaman uyumsuz / beklemede olduğu gibi SO, haritadan nasıl çıktıklarını anlamayan kullanıcılardan gelen sorularla doludur. Oldukça iyi bir uyarı alsalar bile. Şimdi bu onların sorunu, C # takımının sorunu değil. Ejderhaları işaretlediler.
Hans Passant

1
@Sayse, eşzamanlı ve eşzamansız işlevler arasındaki ayrımı kaldırsanız bile, eşzamanlı olarak uygulanan işlevlerin çağrılması hala eşzamanlı olacaktır (WriteLine örneğiniz gibi)
talkol

2
"Dönüş değerini Task <> ile sarın" Yönteminizin asyncolması gerekmedikçe (bazı sözleşmeleri yerine getirmek için), bu muhtemelen kötü bir fikirdir. Zaman uyumsuzluğun dezavantajlarını (yöntem çağrılarının artan maliyeti; awaitçağıran kodda kullanma zorunluluğu) elde edersiniz , ancak hiçbir avantajı yoktur.
svick

1
Bu gerçekten ilginç bir soru, ancak SO için pek uygun olmayabilir. Programcılara taşımayı düşünebiliriz.
Eric Lippert

1
Belki bir şeyi kaçırıyorum, ama bende tam olarak aynı sorum var. Async / await her zaman çiftler halinde gelirse ve kodun yürütülmesini bitirmesini beklemesi gerekiyorsa, neden yalnızca mevcut .NET çerçevesini güncellemiyorsunuz ve ek anahtar sözcükler OLUŞTURMADAN varsayılan olarak zaman uyumsuz - zaman uyumsuz olması gereken yöntemleri yapmıyorsunuz? Dil zaten kaçmak için tasarlandığı şeye dönüşüyor - anahtar kelime spagetti. "Var" önerildikten sonra bunu yapmayı bırakmaları gerektiğini düşünüyorum. Şimdi "dinamik", asyn / await ... vs var ... Neden sadece .NET-ify javascript yapmıyorsunuz? ;))
monstro

Yanıtlar:


127

Öncelikle, nazik sözleriniz için teşekkür ederim. Gerçekten harika bir özellik ve küçük bir parçası olduğum için mutluyum.

Tüm kodum yavaşça eşzamansız hale geliyorsa, neden hepsini varsayılan olarak eşzamansız hale getirmiyorsunuz?

Eh, abartıyorsun; tüm kodunuz eşzamansız hale gelmiyor. İki "düz" tam sayıyı birbirine eklediğinizde, sonucu beklemiyorsunuz. Gelecekte üçüncü bir tam sayı elde etmek için gelecekteki iki tamsayıyı topladığınızda - çünkü bu Task<int>, gelecekte erişebileceğiniz bir tam sayıdır - tabii ki büyük olasılıkla sonucu bekliyorsunuzdur.

Her şeyi eşzamansız yapmamanın birincil nedeni , eşzamansız / beklemenin amacının, birçok yüksek gecikme işleminin olduğu bir dünyada kod yazmayı kolaylaştırmak olmasıdır . İşlemlerinizin büyük çoğunluğu yüksek gecikme süreleri değildir , bu nedenle bu gecikmeyi azaltan performans vuruşunu almak mantıklı değildir. Aksine, işlemlerinizden önemli birkaçı yüksek gecikmedir ve bu işlemler kod boyunca zombi istilasına neden olan zaman uyumsuzluğuna neden olur.

tek sorun performans ise, kesinlikle bazı akıllı optimizasyonlar gerekli olmadığında ek yükü otomatik olarak kaldırabilir.

Teoride teori ve pratik benzerdir. Pratikte asla değildirler.

Bu tür bir dönüşüme karşı size üç puan vereyim ve ardından bir optimizasyon geçişi vereyim.

Yine ilk nokta şudur: C # / VB / F #'daki asenkron, esasen sınırlı bir devam etme biçimidir . İşlevsel dil topluluğundaki muazzam miktarda araştırma, devamlı geçiş stilinden yoğun bir şekilde yararlanan kodun nasıl optimize edileceğini belirleme yollarını bulmaya gitti. Derleyici ekibinin, "eşzamansız" ın varsayılan olduğu ve eşzamansız olmayan yöntemlerin tanımlanması ve eşzamansız hale getirilmesi gereken bir dünyada muhtemelen çok benzer sorunları çözmesi gerekecektir. C # ekibi, açık araştırma problemlerini üstlenmekle pek ilgilenmiyor, bu yüzden burada büyük noktalar var.

İkinci bir nokta, C # 'nın bu tür optimizasyonları daha izlenebilir kılan "referans şeffaflık" seviyesine sahip olmamasıdır. "Referans şeffaflığı" ile , bir ifadenin değerinin değerlendirildiğinde bağlı olmadığı özelliği kastediyorum . Gibi 2 + 2ifadeler referans olarak şeffaftır; İsterseniz değerlendirmeyi derleme zamanında yapabilir veya çalışma zamanına erteleyerek aynı cevabı alabilirsiniz. Ancak x ve y zamanla değiştiğix+y için gibi bir ifade zaman içinde hareket ettirilemez .

Async, bir yan etkinin ne zaman olacağı konusunda akıl yürütmeyi çok daha zor hale getirir. Eşzamansızdan önce,

M();
N();

ve M()oldu void M() { Q(); R(); }ve N()oldu void N() { S(); T(); }ve Rve Süretim yan etkileri, o zaman R'ın yan etkisi S'nin yan etkisi önce olur biliyorum. Ama eğer async void M() { await Q(); R(); }öyleyse aniden pencereden dışarı çıkar. Önce mi R()yoksa sonra mı olacağına dair hiçbir garantiniz yok S()(tabii ki M()beklenmedikçe; ama tabii ki Tasksonrasına kadar beklenmesine gerek yok N().)

Şimdi, hangi sırayla yan etkilerin oluştuğunu artık bilmeme özelliğinin, programınızdaki , optimize edicinin eşzamansız hale getirmeyi başardığı kodlar dışındaki her kod parçası için geçerli olduğunu hayal edin . Temelde artık hangi ifadelerin hangi sırayla değerlendirileceğine dair hiçbir fikriniz yok, bu da tüm ifadelerin referans olarak şeffaf olması gerektiği anlamına geliyor, bu da C # gibi bir dilde zor.

Üçüncü bir nokta da, "eşzamansız neden bu kadar özel?" Diye sormanız gerektiğidir. Her işlemin aslında bir işlem olması gerektiğini tartışacaksanız, Task<T>"neden olmasın Lazy<T>?" Sorusunu yanıtlayabilmeniz gerekir. veya "neden olmasın Nullable<T>?" veya "neden olmasın IEnumerable<T>?" Çünkü biz de bunu kolayca yapabiliriz. Neden her işlemin boş değer atanabilir hale getirilmesi söz konusu olmasın ? Ya da her işlem tembel olarak hesaplanır ve sonuç daha sonrası için önbelleğe alınır veya her işlemin sonucu, tek bir değer yerine bir değerler dizisidir . Daha sonra, "ah, bu asla boş olmamalı, böylece daha iyi kod üretebilirim" gibi durumları optimize etmeye çalışmalısınız.

Demek istediğim: Task<T>Bu kadar çalışmayı garanti etmenin aslında o kadar özel olduğu benim için net değil .

Bu tür şeyler ilginizi çekiyorsa, Haskell gibi çok daha güçlü bir bilgi şeffaflığına sahip olan ve her türlü sıra dışı değerlendirmeye izin veren ve otomatik önbelleğe alma yapan işlevsel dilleri araştırmanızı tavsiye ederim. Haskell ayrıca, bahsettiğim "monadik kaldırma" türleri için kendi tip sisteminde çok daha güçlü bir desteğe sahip.


Await olmadan çağrılan zaman uyumsuz işlevlere sahip olmak bana mantıklı gelmiyor (yaygın durumda). Bu özelliği kaldıracak olsaydık, derleyici bir fonksiyonun asenkron olup olmadığına kendi başına karar verebilir (await'i çağırır mı?). O zaman her iki durum için de aynı sözdizimine sahip olabiliriz (eşzamansız ve eşzamanlı) ve yalnızca çağrılarda farklılaştırıcı olarak await'i kullanabiliriz. Zombi istilası çözüldü :)
talkol

İsteğiniz üzerine programcılardaki tartışmaya devam ettim: programmers.stackexchange.com/questions/209872/…
talkol

@EricLippert - Her zamanki gibi çok güzel cevap :) "Yüksek gecikme" yi açıklayabilir misiniz? Burada milisaniye cinsinden genel bir aralık var mı? Asenkron kullanım için alt sınır çizgisinin nerede olduğunu anlamaya çalışıyorum çünkü onu kötüye kullanmak istemiyorum.
Travis J

7
@TravisJ: Kılavuz: UI iş parçacığını 30 ms'den fazla engellemeyin. Bundan daha fazlası olursa, duraklamanın kullanıcı tarafından fark edilmesi riskini alırsınız.
Eric Lippert

1
Beni asıl zorlayan şey, bir şeyin eşzamanlı veya eşzamansız yapılıp yapılmamasının değişebilen bir uygulama ayrıntısı olmasıdır. Ancak bu uygulamadaki değişiklik, onu çağıran kod, onu çağıran kod vb. Aracılığıyla bir dalgalanma etkisini zorlar. Bağımlı olduğu kod nedeniyle kodu değiştiriyoruz, bu normalde kaçınmak için çok uğraştığımız bir şey. Ya da async/awaitsoyutlama katmanlarının altındaki gizli bir şey eşzamansız olabileceği için kullanırız.
Scott Hannen

23

Neden tüm işlevler eşzamansız olmamalı?

Bahsettiğiniz gibi performans bir nedendir. Bağlandığınız "hızlı yol" seçeneğinin, tamamlanmış bir Görev durumunda performansı artırdığını, ancak yine de tek bir yöntem çağrısına kıyasla çok daha fazla talimat ve ek yük gerektirdiğini unutmayın. Bu nedenle, "hızlı yol" yerinde olsa bile, her zaman uyumsuz yöntem çağrısıyla çok fazla karmaşıklık ve ek yük ekliyorsunuz.

Geriye dönük uyumluluğun yanı sıra diğer dillerle uyumluluk (birlikte çalışma senaryoları dahil) da sorunlu hale gelebilir.

Diğeri ise karmaşıklık ve niyet meselesidir. Eşzamansız işlemler karmaşıklık katar - çoğu durumda dil özellikleri bunu gizler, ancak yöntem yapmanın asynckullanımlarına kesinlikle karmaşıklık kattığı birçok durum vardır . Bu, özellikle bir senkronizasyon bağlamınız yoksa geçerlidir, çünkü zaman uyumsuz yöntemler daha sonra kolayca beklenmeyen iş parçacığı sorunlarına neden olabilir.

Ek olarak, doğası gereği asenkron olmayan birçok rutin vardır. Bunlar senkron işlemler olarak daha mantıklıdır. Zorlamak Math.Sqrtolmak Task<double> Math.SqrtAsyncasenkron olmak bunun için hiç bir sebep yok, örneğin, saçma olurdu. asyncBaşvurunuzu zorlamak yerine, her yerdeawait yayılırsınız .

Bu aynı zamanda mevcut paradigmayı tamamen bozacak ve özelliklerle ilgili sorunlara neden olacaktır (bunlar aslında sadece yöntem çiftleridir .. onlar da eşzamansız mı olurlar?) Ve çerçeve ve dilin tasarımı boyunca başka yankılara yol açar.

Çok fazla IO bağlantılı iş yapıyorsanız, asyncyaygın olarak kullanmanın harika bir ek olduğunu bulma eğiliminde olacaksınız , rutinlerinizin çoğu olacaktır async. Bununla birlikte, CPU'ya bağlı iş yapmaya başladığınızda, genel olarak, bir şeyler asyncyapmak aslında iyi değildir - bu , asenkron gibi görünen , ancak gerçekten de gerçekten asenkron olmayan bir API altında CPU döngüleri kullandığınız gerçeğini gizler .


Tam olarak yazacağım şey (performans), geriye dönük uyumluluk başka bir şey olabilir,
dll'ler zaman uyumsuzluğu

Eşitleme ve eşzamansız işlevler arasındaki ayrımı kaldırırsak sqrt eşzamansız yapmak saçma olmaz
talkol

@talkol Ben Bunu tersine çevirmek herhalde - neden olmalıdır asenkronisi karmaşıklığına her işlev çağrısı almak?
Reed Copsey

2
@talkol Bunun mutlaka doğru olmadığını iddia ediyorum - eşzamansızlık engellemekten daha kötü hataların kendisini ekleyebilir ...
Reed Copsey

1
@talkol Nasıl daha await FooAsync()basit Foo()? Ve bazen küçük domino etkisi yerine, her zaman büyük bir domino etkisine sahipsiniz ve buna bir gelişme mi diyorsunuz?
svick

4

Performans bir yana - eşzamansız bir üretkenlik maliyeti olabilir. İstemcide (WinForms, WPF, Windows Phone) üretkenlik için bir nimettir. Ancak sunucuda veya diğer UI olmayan senaryolarda üretkenlik ödersiniz . Kesinlikle orada varsayılan olarak eşzamansız gitmek istemezsiniz. Ölçeklenebilirlik avantajlarına ihtiyaç duyduğunuzda kullanın.

Tatlı noktadayken kullanın. Diğer durumlarda, yapma.


2
+1 - Rastgele tamamlanma sırasına paralel olarak 5 eşzamansız işlem çalıştıran bir zihinsel kod haritasına sahip olmaya yönelik basit bir girişim, çoğu insan için bir gün için yeterli acı sağlayacaktır. Eşzamansız (ve dolayısıyla doğası gereği paralel) kodun davranışı hakkında mantık yürütmek, eski eşzamanlı koddan çok daha zordur ...
Alexei Levenkov

2

Genişletilebilirlik gerekmiyorsa tüm yöntemleri eşzamansız hale getirmek için iyi bir neden olduğuna inanıyorum. Seçici yapma yöntemleri zaman uyumsuzluğu yalnızca kodunuz hiçbir zaman gelişmediğinde ve A () yönteminin her zaman CPU'ya bağlı olduğunu (eşitlemeyi sürdürürsünüz) ve yöntem B'nin () her zaman G / Ç'ye bağlı olduğunu (eşzamansız olarak işaretlerseniz) bilirseniz çalışır.

Ama ya işler değişirse? Evet, A () hesaplamalar yapıyor ancak gelecekte bir noktada oraya günlük kaydı veya raporlama veya öngörülemeyen uygulama ile kullanıcı tanımlı geri arama eklemeniz gerekiyordu veya algoritma genişletildi ve şimdi sadece CPU hesaplamalarını değil, aynı zamanda ayrıca bazı G / Ç? Yöntemi eşzamansız hale getirmeniz gerekir, ancak bu API'yi bozar ve yığındaki tüm arayanların da güncellenmesi gerekir (ve hatta farklı satıcılardan farklı uygulamalar bile olabilirler). Ya da senkronizasyon versiyonunun yanına asenkron versiyon eklemeniz gerekir, ancak bu pek bir fark yaratmaz - senkronizasyon versiyonunun kullanılması engellenir ve bu nedenle pek kabul edilemez.

API'yi değiştirmeden mevcut senkronizasyon yöntemini eşzamansız hale getirmek mümkün olsaydı harika olurdu. Ancak gerçekte böyle bir seçeneğimiz olmadığına inanıyorum ve şu anda ihtiyaç duyulmasa bile zaman uyumsuz sürümü kullanmak, gelecekte uyumluluk sorunlarına asla çarpmayacağınızı garanti etmenin tek yoludur.


Bu aşırı bir yorum gibi görünse de, içinde pek çok gerçek var. Birincisi, bu sorundan dolayı: "Bu, API'yi bozar ve tüm arayanlar yığında kalır" async / await, bir uygulamayı daha sıkı bir şekilde birleştirir. Örneğin, bir alt sınıf eşzamansız / beklemeyi kullanmak isterse, Liskov Değiştirme ilkesini kolayca bozabilir. Ayrıca, çoğu yöntemin zaman uyumsuz / beklemeye ihtiyaç duymadığı bir mikro hizmet mimarisi hayal etmek zordur.
ItsAllABadJoke
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.