İyi mimari ve soyutlama katmanlama kavramlarıyla çelişen C # 'daki async / await kullanım kılavuzları değil mi?


103

Bu soru C # diliyle ilgilidir, ancak Java veya TypeScript gibi diğer dilleri kapsamasını bekliyorum.

Microsoft , .NET'te zaman uyumsuz çağrıları kullanma konusunda en iyi uygulamaları önerir . Bu tavsiyeler arasında, iki tane seçelim:

  • Eşzamansız yöntemlerin imzasını değiştirerek Görev veya Görev <> öğelerini döndürürler (TypeScript'te bir Söz <> olur)
  • xxxAsync () ile bitecek async yöntemlerinin adlarını değiştirin

Şimdi, düşük seviyeli, senkronize bir komponenti bir eşzamansız olan ile değiştirirken, bu uygulamanın tam yığınını etkiler. Eşzamansız / beklemede yalnızca "tamamen kullanılırsa" olumlu bir etkiye sahip olduğu için, uygulamadaki her katmanın imzası ve yöntem adlarının değiştirilmesi gerektiği anlamına gelir.

İyi bir mimari çoğu zaman düşük seviyeli bileşenlerin başkaları tarafından değiştirilmesinin üst seviye bileşenler tarafından görülmeyeceği şekilde her bir tabaka arasına soyutlamalar yerleştirmeyi içerir. C # 'da, soyutlamalar arayüz şeklini alır. Yeni, düşük seviyeli, zaman uyumsuz bir bileşen tanıtırsak, çağrı yığınındaki her arabirimin yeni bir arabirimle değiştirilmesi veya değiştirilmesi gerekir. Bir uygulayıcı sınıfta bir problemin çözülme şekli (asenkron veya senkronize) artık arayanlara gizlenmemiş (soyutlanmış). Arayan kişinin senkronize mi, yoksa senkronize mi olmadığını bilmek zorunda.

Async / "iyi mimari" ilkelerine aykırı en iyi uygulamaları beklemiyor musunuz?

Her bir arayüzün (örneğin, IEnumerable, IDataAccessLayer demek) async muadillerine (IAsyncEnumerable, IAsyncDataAccessLayer) ihtiyacı olduğu anlamına gelir mi?

Sorunu biraz daha ileriye götürürsek, her yöntemin eşzamansız olduğunu varsaymak (bir Görev <> veya Söz <> döndürmek) ve gerçekte olmadıklarında eşzamansız çağrıları senkronize etme yöntemleri için daha kolay olmaz mıydı? zaman uyumsuz? Gelecekteki programlama dillerinden beklenen bir şey mi?


5
Bu harika bir tartışma sorusu gibi görünse de, bunun burada cevaplanmayacak kadar görüşe dayalı olduğunu düşünüyorum.
Öforik

22
@Euphoric: Burada çizilen sorunun C # kurallarından daha derine indiğini düşünüyorum, bu sadece bir uygulamanın parçalarının asenkron davranışa dönüşmesinin genel sistem üzerinde yerel olmayan etkilere sahip olabileceğinin bir belirtisidir. Bu yüzden bağırsaklarım, bunun için teknik gerçeklere dayanarak düşünülmeyen bir cevap olması gerektiğini söyledi. Bu nedenle, buradaki herkesi bu soruyu çok erken kapatmamaya teşvik ediyorum, bunun yerine hangi cevapların geleceğini bekleyelim (ve eğer çok fazla fikirleri varsa, hala kapanma için oy kullanabiliriz).
Doktor Brown,

25
@DocBrown Burada daha derin bir soru var: "Sistemin bir parçası senkronize olandan senkronize edilemez ve buna bağlı parçaları da değişmek zorunda kalmadan değiştirilebilir mi?" Bence cevabı açık "hayır" dır. Bu durumda, burada "iyi mimarlık ve katmancılık kavramlarının" nasıl uygulandığını anlamıyorum.
Öforik

6
@Euphoric: düşüncesiz bir cevap için iyi bir temel gibi geliyor ;-)
Doc Brown

5
@Gmanman: C #, birçok dil gibi, yalnızca geri dönüş türüne göre aşırı yükleme yapamaz. Eşitleme meslektaşlarıyla aynı imzayı taşıyan asenkron yöntemlerle bitirdiniz (hepsi alamaz CancellationToken, ve bir temerrüde ulaşmak isteyenler). Mevcut senkronizasyon yöntemlerini kaldırmak (ve proaktif olarak tüm kodları kırmak) açık bir başlangıç ​​değildir.
Jeroen Mostert

Yanıtlar:


111

İşleviniz Ne Renk?

İlginizi çekebilir Bob Nystrom'un İşlevinizdeki Renk Nedir 1 .

Bu yazıda, kurgusal bir dili şöyle açıklar:

  • Her işlevin bir rengi vardır: mavi veya kırmızı.
  • Kırmızı bir işlev mavi veya kırmızı işlevler olarak adlandırılabilir, sorun olmaz.
  • Mavi işlev yalnızca mavi işlevleri çağırabilir.

Hayali olsa da, bu programlama dillerinde oldukça düzenli olur:

  • C ++ 'da bir "const" yöntemi sadece diğer "const" yöntemlerini çağırabilir this.
  • Haskell'de, bir IO olmayan fonksiyon sadece IO olmayan fonksiyonları çağırabilir.
  • C # 'da, bir senkronizasyon işlevi yalnızca senkronizasyon işlevlerini 2 arayabilir .

Fark ettiğiniz gibi, bu kurallar nedeniyle, kırmızı fonksiyonlar kod tabanının etrafına yayılma eğilimindedir . Bir tane eklersiniz ve yavaş yavaş tüm kod tabanını kolonize eder.

1 Bob Nystrom, bloglamanın yanı sıra Dart ekibinin bir parçası ve bu küçük Crafting Interpreters serisini yazdı; Herhangi bir programlama dili / derleyici afficionado için şiddetle tavsiye edilir.

2 Asenkron bir işlev çağırabildiğiniz ve geri dönene kadar engelleyebileceğiniz için pek doğru değil, ancak ...

Dil Sınırlaması

Bu, esasen, bir dil / çalışma zamanı sınırlamasıdır.

M: Dil, örneğin Erlang ve Go gibi N diş ipliği, asyncfonksiyonlara sahip değildir : her bir fonksiyon potansiyel olarak eşzamansızdır ve "lifi" basitçe askıya alınacak, değiştirilecektir ve tekrar hazır olduğunda geri alınacaktır.

C #, 1: 1 iş parçacığı modeliyle birlikte gitti ve bu nedenle, dişleri yanlışlıkla tıkamaktan kaçınmak için dilde senkronize olmaya karar verdi.

Dil kısıtlamaları varlığında, kodlama yönergeleri uyum sağlamak zorundadır.


4
G / Ç işlevleri, yayılma eğilimine sahiptir, ancak, titizlikle, bunları çoğunlukla kodunuzun giriş noktalarına yakın (arama sırasında yığında) işlevlere göre ayırabilirsiniz. Bunu, bu fonksiyonların IO fonksiyonlarını çağırmasını ve ardından diğer fonksiyonların onlardan çıkışı işlemesini sağlayarak ve daha fazla IO için ihtiyaç duyulan sonuçları geri getirerek yapabilirsiniz. Bu tarzın kod tabanlarımı yönetmeyi ve çalışmayı daha kolay hale getirdiğini biliyorum. Eşzamanlılığın bir sonucu olup olmadığını merak ediyorum.
jpmc26

16
"M: N" ve "1: 1" dişleriyle ne demek istiyorsun?
Kaptan Adam

14
@CaptainMan: 1: 1 thread, bir uygulama dizisini bir işletim sistemi dizisine eşlemek anlamına gelir; bu, C, C ++, Java veya C # gibi dillerde geçerlidir. Buna karşılık, M: N iş parçacığı, M uygulama iş parçacıklarını N işletim sistemi iş parçacıklarıyla eşlemek anlamına gelir; Go durumunda, bir uygulama iş parçacığına "goroutine" denir, Erlang durumunda "aktör" olarak adlandırılır ve bunları "yeşil iplikler" veya "lifler" olarak da duymuş olabilirsiniz. Paralellik gerektirmeden eşzamanlılık sağlarlar. Maalesef konuyla ilgili Wikipedia makalesi oldukça seyrek.
Matthieu M.

2
Bu biraz ilişkili ama aynı zamanda bu "işlev rengi" fikrinin de kullanıcının girişini engelleyen işlevler için geçerli olduğunu düşünüyorum; çerçeve başından beri olmuştur.
jrh

2
@MatthieuM. C #, bir işletim sistemi iş parçacığı için bir uygulama iş parçacığına sahip değil ve hiç olmadı. Yerel kodla etkileşim kurduğunuzda bu çok açık ve özellikle MS SQL'de çalışırken belirgindir. Ve elbette, işbirlikçi rutinler her zaman mümkündü (ve daha da basitleşiyorlardı async); Aslında, duyarlı UI oluşturmak için oldukça yaygın bir kalıptı. Erlang kadar güzel miydi? Hayır! Ama bu hala C :) 'den uzak bir ağlama
Luaan

82

Burada haklısın, burada bir çelişki var, ama kötü olan "en iyi uygulamalar" değil. Bunun nedeni asenkron fonksiyonun esas olarak senkron fonksiyondan farklı bir şey yapmasıdır. Sonucu bağımlılıklarından beklemek yerine (genellikle bazı IO) ana olay döngüsü tarafından ele alınacak bir görev yaratır. Bu, soyutlama altında iyi gizlenebilecek bir fark değildir.


27
Cevap bu IMO kadar basit. Eşzamanlı ve eşzamansız işlem arasındaki fark bir uygulama detayı değildir - anlamsal olarak farklı bir sözleşmedir.
Karınca

11
@AntP: Bu kadar basit olduğuna katılmıyorum; örneğin C # dilinde, fakat Go dilinde değil. Bu, asenkron işlemlerin doğal bir özelliği değildir, asenkron işlemlerin verilen dilde nasıl modellendiği meselesidir.
Matthieu M.

1
@MatthieuM. Evet, ancak aynı asynczamanda senkronize sözleşmeler yapmak için C # yöntemlerini kullanabilirsiniz . Tek fark Go'nun varsayılan olarak eşzamansız olması, C # ise varsayılan olarak eşzamanlı olmasıdır. asyncsize ikinci programlama modelini verir - async soyutlamadır (gerçekte yaptığı şey çalışma zamanına, görev zamanlayıcısına, senkronizasyon içeriğine, bekleyen uygulamasına bağlıdır ...).
Luaan

6

Eşzamansız bir yöntem, farkında olduğunuzdan emin olarak eşzamanlı olandan farklı davranır. Çalışma zamanında, zaman uyumsuz bir çağrıyı zaman uyumlu bir çağrıya dönüştürmek önemsizdir, ancak bunun tam tersi söylenemez. Bu nedenle mantık o zaman olur, neden gerektirebilecek her yöntemin asenkron yöntemlerini yapmıyoruz ve arayanı senkronize bir yönteme gerektiği gibi "dönüştürüyor"?

Bir anlamda, istisnaları fırlatan, "güvenli" olan ve hata durumunda bile atmayacak bir metoda sahip olmak gibidir. Kodlayıcı hangi noktada, aksi takdirde bir diğerine dönüştürülebilen bu yöntemleri sağlamak için aşırıdır?

Bu konuda iki düşünce okulu vardır: biri birden çok yöntem oluşturmaktır, her biri diğerine özel yöntem olarak adlandırılır, isteğe bağlı parametreler veya asenkron olma gibi davranışlarda küçük değişiklikler yapma imkanı sunar. Diğeri, gerekli değişiklikleri kendisinin yapması için arayanın üzerine bırakarak gerekli olanları kaldırmak için arayüz yöntemlerini en aza indirmektir.

İlk okuldaysanız, her aramayı iki katına çıkarmaktan kaçınmak için bir sınıfı senkronize ve asenkron aramalara adama konusunda belirli bir mantık vardır. Microsoft, bu düşünce okulunu destekleme eğiliminde ve konvansiyonel olarak Microsoft'un tercih ettiği stille tutarlı kalmak için siz de bir Async sürümüne sahip olmanız gerekecek, tıpkı arayüzlerin neredeyse her zaman "I" ile başlaması gibi. Yanlış olmadığını vurgulamama izin verin , çünkü "doğru yoldan" değil, projede tutarlı bir stil elde etmek ve bir projeye eklediğiniz gelişim için stilinizi kökten değiştirmek daha iyidir.

Bununla birlikte, arayüz yöntemlerini en aza indirgemek olan ikinci okulu tercih etme eğilimindeyim. Bir yöntemin eşzamansız bir şekilde çağrılabileceğini düşünüyorsanız, benim için bu yöntem eşzamansızdır. Arayan, devam etmeden önce bu görevin bitmesini bekleyip beklemeyeceğine karar verebilir. Bu arayüz bir kütüphanenin arayüzü ise, kullanımdan kaldırmanız veya ayarlamanız gereken metot sayısını en aza indirmek için bu şekilde yapmak daha makul olacaktır. Arayüz projemde dahili kullanım içinse, projem boyunca ihtiyaç duyduğum her çağrı için sağlanan parametreler için bir yöntem ekleyeceğim ve "ekstra" yöntem yok, ve o zaman bile, yalnızca yöntemin davranışı zaten kapsanmamışsa mevcut bir yöntemle.

Ancak, bu alandaki birçok şey gibi, büyük ölçüde özneldir. Her iki yaklaşımın da artıları ve eksileri var. Microsoft ayrıca değişken isminin başında tür belirten harfler ekleme ve "m_" gibi bir değişken olduğunu ispatlayan bir sözleşmeye başladı m_pUser. Demek istediğim, Microsoft'un bile yanılmaz olması ve hatalar yapabilmesi.

Bu, eğer projeniz bu Async sözleşmesini izliyorsa, ona saygı duymanızı ve stile devam etmenizi öneririm. Ve sadece bir kez kendinize ait bir proje verildiğinde, uygun gördüğünüz şekilde yazabilirsiniz.


6
"Çalışma zamanında, zaman uyumsuz bir çağrıyı senkronize bir çağrıya dönüştürmek önemsizdir" Tam olarak öyle olduğundan emin değilim. .NET'te, .Wait()yöntem ve benzeri yöntemlerin kullanılması olumsuz sonuçlara neden olabilir ve js'de bildiğim kadarıyla hiçbir şekilde mümkün değildir.
maksimum 630

2
@ max630 Göz önünde bulundurulması gereken eşzamanlı sorun olmadığını söylemedim, ancak başlangıçta senkronize bir görev olsaydı , şans kilitlenme yaratmıyor. Yani, önemsiz "senkronize dönüştürmek için buraya çift tıklayın" anlamına gelmez. Js'de bir Promise örneği döndürürsünüz ve bu konuda çözümleme çağrısı yaparsınız.
Neil

2
evet, eşzamanlı olarak eşzamansız olanı tekrar eşzamanlı hale dönüştürmek için bir acı
Ewan

4
@Neil Javascript'te, Promise.resolve(x)onu geri çağırıp sonra geri aramalar eklemiş olsanız bile , bu geri aramalar derhal yürütülmez.
NickL

1
@ Ne bir arayüz eşzamansız bir yöntem ortaya koyarsa, Görevde beklemenin bir kilitlenme yaratmayacağını tahmin etmek iyi bir varsayım değildir. Arabirimin, yöntem imzasında aslında eşzamanlı olduğunu göstermesi, belgelerde daha sonraki bir sürümde değişebilecek bir sözden daha iyidir.
Carl Walsh,

2

İmzalarını değiştirmeden işlevleri zaman uyumsuz bir şekilde çağırmanın bir yolunun olduğunu hayal edelim.

Bu gerçekten harika olurdu ve hiç kimse isimlerini değiştirmenizi tavsiye etmedi.

Ancak gerçek asenkron fonksiyonlar, sadece başka bir asenkron fonksiyon bekleyenler değil, en düşük seviyenin ise asenkron doğalarına özgü bazı yapıları vardır. Örneğin

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

Bu, kodun, şeylerin hoşuna giden Taskve asynckapsülleyici bir şekilde asenkron bir şekilde çalıştırılması için çağıran kodun ihtiyaç duyduğu iki bilgidir .

Eşzamansız işlev yapmanın birden fazla yolu vardır, async Taskgeri dönüş türleriyle derleyiciye yerleşik tek bir şablondur, böylece olayları el ile bağlamak zorunda kalmazsınız


0

Ana noktaya daha az C # ness modası ve daha genel bir şekilde değineceğim:

Async / "iyi mimari" ilkelerine aykırı en iyi uygulamaları beklemiyor musunuz?

Bunun sadece API'nizin tasarımında yaptığınız seçime ve kullanıcıya neye izin verdiğinize bağlı olduğunu söyleyebilirim.

API'nizin bir işlevinin yalnızca eşzamansız olmasını istiyorsanız, adlandırma kurallarını izlemeye çok az ilgi vardır. Döndürme türü olarak sadece her zaman Görev <> / Promise <> / Gelecek <> / ... 'ı iade edin, kendi kendini belgelendirir. Senkronize bir cevap istiyorsa, bunu bekleyerek yine de yapabilecektir, ancak her zaman bunu yaparsa, bir miktar kazan yapar.

Ancak, yalnızca API senkronizasyonunuzu yaparsanız, kullanıcı asenkronize etmek isterse asenkron bölümünü kendisi yönetmek zorunda kalacağı anlamına gelir.

Bu oldukça fazladan ek iş yapabilir, ancak aynı zamanda kaç eşzamanlı çağrıya izin verdiği, zaman aşımı, yeniden deneme vb. Durumları konusunda kullanıcıya daha fazla kontrol sağlayabilir.

Büyük bir API'ye sahip büyük bir sistemde, çoğunu varsayılan olarak senkronize etmek üzere uygulamak, özellikle kaynaklarını (dosya sistemi, CPU, veritabanı, ...) paylaşıyorlarsa, API'nizin her bir bölümünü bağımsız olarak yönetmekten daha kolay ve daha verimli olabilir.

Aslında, en karmaşık kısımlar için, API'nizin aynı bölümünün iki uygulamasına mükemmel şekilde sahip olabilirsiniz, biri kullanışlı şeyleri senkronize ederken, biri senkronize olana senkronize olmayan birini kullanıyor, yalnızca eşzamanlılığı, yükleri, zaman aşımlarını ve yeniden denemeleri yönetir .

Belki başkası onunla deneyimlerini paylaşabilir çünkü bu tür sistemlerle ilgili deneyimim yok.


2
@Miral Her iki olasılıkta da "senkronizasyon yönteminden zaman uyumsuz bir yöntem çağrısı" kullandınız.
Adrian Wragg

@AdrianWragg Ben yaptım; beynimin bir yarış durumu olmuş olmalı. Düzelteceğim.
Miral

Öteki yol bu; Eşitleme yönteminden bir eşzamansız yöntem çağırmak çok önemlidir, ancak eşzamansız bir yöntemden eşzamanlı yöntem çağırmak mümkün değildir. (Ve işlerin tamamen parçalandığı yer, biri yine de bunu yapmayı denediğinde, ki bu kilitlenmelere neden olabilir.) Eğer birini seçmek zorunda kalırsanız, varsayılan olarak, zaman zaman uyumsuzluk en iyi seçimdir. Ne yazık ki, aynı zamanda daha zor bir seçim, çünkü bir eşzamansız uygulama yalnızca eşzamansız yöntemler diyebilir.
Miral

(Ve bununla elbette engelleyici bir senkronizasyon yöntemi demek istiyorum . Eşzamansız bir işlemciden saf işlemcili hesaplama yapan bir eşzamansız yöntemle eşzamanlı olarak çağırabilirsiniz - ancak bir işçi bağlamında olmadığınızı bilmeden bunu yapmaktan kaçınmalısınız UI bağlamı yerine - ancak kilitte veya G / Ç için veya başka bir işlem için boşta bekleyen aramaları engellemek kötü bir fikirdir.)
Miral
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.