Kodun hem zaman uyumsuz hem de senkronizasyon sürümlerine sahip olmanız gerektiğinde DRY ilkesini ihlal etmekten nasıl kaçınabilirsiniz?


15

Aynı mantık / yöntemin hem asenkron hem de senkronizasyon sürümünü desteklemesi gereken bir proje üzerinde çalışıyorum. Yani örneğin:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

Bu yöntemler için eşzamansız ve eşzamanlama mantığı, biri eşzamansız ve diğerinin olmaması dışında her açıdan aynıdır. Bu tür bir senaryoda KURU prensibini ihlal etmekten kaçınmanın meşru bir yolu var mı? İnsanların, zaman uyumsuz bir yöntemde GetAwaiter (). Bu iş parçacığı tüm senaryolarda güvenli midir? Bunu yapmanın başka, daha iyi bir yolu var mı veya mantığı çoğaltmak zorunda mıyım?


1
Burada ne yaptığınızdan biraz daha bahsedebilir misiniz? Özellikle, eşzamansız yönteminizde eşzamansızlığa nasıl ulaşıyorsunuz? Yüksek gecikmeli çalışma CPU'su veya GÇ bağlı mı?
Eric Lippert

"biri zaman uyumsuz diğeri değil" gerçek yöntemin kod veya sadece arayüz farkı nedir? (Açıkçası sadece arayüz için yapabilirsiniz return Task.FromResult(IsIt());)
Alexei Levenkov

Ayrıca, muhtemelen senkron ve asenkron versiyonların her ikisi de yüksek gecikmedir. Arayan hangi koşullar altında eşzamanlı sürümü kullanacak ve bu arayan arayanın milyarlarca nanosaniye alabileceği gerçeğini nasıl azaltacak? Senkron versiyonun arayanı, kullanıcı arayüzünü asmaları umurunda değil mi? Kullanıcı arayüzü yok mu? Bize senaryo hakkında daha fazla bilgi verin.
Eric Lippert

@EricLippert Size, 2 kod tabanının async olması dışında nasıl aynı olduğunu anlamanız için belirli bir kukla örnek ekledim. Bu yöntemin çok daha karmaşık olduğu ve kod satırlarının ve satırlarının çoğaltılması gereken bir senaryoyu kolayca hayal edebilirsiniz.
Marko

@AlexeiLevenkov Fark gerçekten kod, demo için bazı kukla kod ekledim.
Marko

Yanıtlar:


15

Sorunuzda birkaç soru sordunuz. Onları senden biraz farklı bir şekilde parçalayacağım. Ama önce soruyu doğrudan cevaplayayım.

Hepimiz hafif, yüksek kaliteli ve ucuz bir kamera istiyoruz, ancak deyişe göre, bu üçten en fazla ikisini alabilirsiniz. Burada aynı durumdasınız. Senkronize ve senkronize olmayan yollar arasında verimli, güvenli ve kodu paylaşan bir çözüm istiyorsunuz. Onlardan sadece ikisini alacaksın.

Bunun nedenini yıkayım. Bu soru ile başlayacağız:


İnsanların GetAwaiter().GetResult()bir zaman uyumsuz yöntemde kullanabileceğinizi ve bunu senkronizasyon yönteminizden çağırabileceğini söylediğini gördüm. Bu iş parçacığı tüm senaryolarda güvenli midir?

Bu sorunun amacı, "eşzamanlı yolun eşzamansız sürümde eşzamanlı bir bekleme yapmasını sağlayarak eşzamanlı ve eşzamansız yolları paylaşabilir miyim?"

Bu noktada çok net olalım, çünkü önemli:

DERHAL OLARAK BU İNSANLARDAN HERHANGİ BİR TAVSİYE ALMALISINIZ .

Bu son derece kötü bir tavsiye. Görevin normal veya anormal şekilde tamamlandığına dair kanıtınız yoksa , eşzamansız bir görevden bir sonucu senkronize olarak almak çok tehlikelidir .

Bunun son derece kötü bir tavsiye olmasının nedeni, bu senaryoyu düşünmektir. Çim biçmek istiyorsunuz, ancak çim biçme bıçağınız kırıldı. Bu iş akışını izlemeye karar verdiniz:

  • Bir web sitesinden yeni bir bıçak sipariş edin. Bu, yüksek gecikmeli, eşzamansız bir işlemdir.
  • Eşzamanlı olarak bekleyin - yani bıçağı elinize alana kadar uyuyun .
  • Bıçağın gelip gelmediğini görmek için posta kutusunu düzenli olarak kontrol edin.
  • Bıçağı kutudan çıkarın. Şimdi elinizde.
  • Bıçağı biçiciye takın.
  • Çimleri biçmek.

Ne oluyor? Sonsuza kadar uyuyorsunuz çünkü postayı kontrol etme işlemi artık posta geldikten sonra gerçekleşen bir şeye bağlı .

Öyle oldukça kolay zaman uyumlu keyfi bir görev beklemek zaman bu durumlarla karşılaşmak. Bu görevin şimdi bekleyen iş parçacığının geleceğinde planlanmış çalışması olabilir ve şimdi bu gelecek asla beklemediğiniz için gelmeyecektir.

Asenkron bir bekleme yaparsanız, o zaman her şey yolunda! Postayı düzenli aralıklarla kontrol edersiniz ve beklerken bir sandviç yapar veya vergilerinizi veya her neyse yaparsınız; beklerken işlerinizi yapmaya devam edersiniz.

Asla eşzamanlı olarak beklemeyin. Görev tamamlandıysa, gereksizdir . Görev tamamlanmadıysa, ancak geçerli iş parçacığının çalışması planlanıyorsa, geçerli iş parçacığı beklemek yerine başka işlere hizmet ediyor olabileceğinden , verimsizdir . Görev tamamlanmazsa ve geçerli iş parçacığında zamanlama çalıştırılırsa, eşzamanlı olarak beklemeye asılı kalır. Görevin tamamlandığını zaten bilmiyorsanız , eşzamanlı olarak beklemek için iyi bir neden yoktur .

Bu konu hakkında daha fazla bilgi için bkz.

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Stephen gerçek dünya senaryosunu benden çok daha iyi açıklıyor.


Şimdi "diğer yönü" ele alalım. Eşzamansız sürümü basitçe bir işçi iş parçacığındaki eşzamanlı sürümü yaparak kod paylaşabilir miyiz?

Yani muhtemelen ve gerçekten de muhtemelen aşağıdaki nedenlerden dolayı kötü bir fikir.

  • Senkron işlemin yüksek gecikmeli IO çalışması olması verimsizdir. Bu aslında bir çalışanı işe alır ve bir görev tamamlanana kadar o çalışanı uyutur . İplikler inanılmaz derecede pahalıdır . Varsayılan olarak minimum bir milyon bayt adres alanı tüketirler, zaman alırlar, işletim sistemi kaynaklarını alırlar; işe yaramaz bir iş yaparak bir iplik yakmak istemezsiniz.

  • Eşzamanlı işlem, iş parçacığı için güvenli olarak yazılmamış olabilir.

  • Bu , yüksek gecikmeli çalışma işlemciye bağlıysa daha makul bir tekniktir, ancak eğer öyleyse muhtemelen bir iş parçacığına teslim etmek istemezsiniz. Muhtemelen görev paralel kütüphanesini mümkün olduğunca çok CPU'ya paralel hale getirmek için kullanmak istersiniz, muhtemelen iptal mantığı istersiniz ve senkronize versiyonun hepsini yapmasını sağlayamazsınız, çünkü o zaman zaten senkronize olmayan versiyon olacaktır .

Daha fazla okuma; Yine, Stephen bunu çok açık bir şekilde açıklıyor:

Neden Görev'i kullanmıyorsunuz?

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Task.Run için daha fazla "yapma ve yapma" senaryosu:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


O zaman bizi ne bırakıyor? Her iki kod paylaşım tekniği ya kilitlenmelere ya da büyük verimsizliklere yol açar. Ulaştığımız sonuç, bir seçim yapmanız gerektiğidir. Verimli ve doğru olan ve arayanı memnun eden bir program mı istiyorsunuz, yoksa eşzamanlı ve eşzamansız yollar arasında az miktarda kod çoğaltarak gerekli birkaç tuş vuruşunu kaydetmek mi istiyorsunuz? İkisini de alamıyorsun, korkuyorum.


Task.Run(() => SynchronousMethod())Eşzamansız sürümde döndürmenin neden OP'nin istediği olmadığını açıklayabilir misiniz ?
Guillaume Sasdy

2
@GuillaumeSasdy: Orijinal poster çalışmanın ne olduğu sorusunu yanıtladı: IO bağlı. Bir işçi iş parçacığında GÇ bağlı görevleri yürütmek israftır!
Eric Lippert

Pekala anladım. Bence haklısın ve ayrıca @StriplingWarrior yanıtı daha da açıklıyor.
Guillaume Sasdy

2
@ İş parçacığını engelleyen neredeyse tüm geçici çözümler sonunda (yüksek yük altında) tüm iş parçacığı iş parçacığı (genellikle zaman uyumsuz işlemleri tamamlamak için kullanılan) tüketir. Sonuç olarak tüm iş parçacıkları bekleyecek ve hiçbiri işlemlerin kodun "eşzamansız işlem tamamlandı" bölümünü çalıştırmasına izin verecek. Çoğu geçici çözüm düşük yük senaryolarında iyidir (her işlemin bir parçası olarak 2-3 hatta çok sayıda iş parçacığı olduğu için)… Ve zaman uyumsuz işlemin tamamlanmasının yeni işletim sistemi iş parçacığında (iş parçacığı havuzu değil) çalışacağını garanti ederseniz tüm durumlar için bile çalışabilir (yüksek fiyat ödersiniz)
Alexei Levenkov

2
Bazen "basitçe bir işçi iş parçacığında zaman uyumlu sürümü yapmak" başka bir yolu yoktur. Örneğin, Dns.GetHostEntryAsync.NET'te ve FileStream.ReadAsyncbelirli dosya türleri için bu şekilde uygulanır . İşletim sistemi basitçe zaman uyumsuz bir arayüz sağlamaz, bu nedenle çalışma zamanının sahte olması gerekir (ve dile özgü değildir - örneğin, Erlang çalışma zamanı , engellemeyen disk G / Ç ve adı sağlamak için her birinin içinde birden çok iş parçacığı olan tüm işçi işlemleri ağacını çalıştırır. çözüm).
Joker_vD

6

Buna tek boyutlu bir cevap vermek zor. Ne yazık ki, eşzamansız ve eşzamanlı kod arasında yeniden kullanım için basit ve mükemmel bir yol yoktur. Ancak, dikkate alınması gereken birkaç ilke şunlardır:

  1. Asenkron ve Senkron kod genellikle temelde farklıdır. Asenkron kod genellikle bir iptal jetonu içermelidir. Ve genellikle farklı yöntemler çağırır (örneğin Query()birinden diğerine çağrı yapar QueryAsync()) veya farklı ayarlarla bağlantılar kurar. Dolayısıyla, yapısal olarak benzer olsa bile, farklı gereksinimlere sahip ayrı bir kod olarak ele alınmasını hak edecek davranışlarda genellikle farklılıklar vardır. File sınıfındaki yöntemlerin Async ve Sync uygulamaları arasındaki farklara dikkat edin, örneğin: aynı kodu kullanmalarını sağlamak için hiçbir çaba sarf edilmez
  2. Bir arabirimi uygulama uğruna bir Eşzamansız yöntem imzası sağlıyorsanız, ancak eşzamanlı bir uygulamanız varsa (yani, yönteminizin ne yaptığına dair asenkron bir şey yok), geri dönebilirsiniz Task.FromResult(...).
  3. Mantığı herhangi bir uyumlu parça olan iki yöntem arasındaki aynı ayrı bir yardımcı yönteme ekstre edilmiş ve her iki yöntemde de desteklenebilir.

İyi şanslar.


-2

Kolay; senkronize olanı zaman uyumsuz olanı çağırın. Bunu Task<T>yapmak için kullanışlı bir yöntem bile var:

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously(); 

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{ 
          task.Wait();
          return task.Result;
      }
      catch(Exception ex) 
      { 
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}

1
Bunun neden asla yapmamanız gereken en kötü uygulama olduğuna dair cevabımı görün.
Eric Lippert

1
Cevabınız GetAwaiter () ile ilgilidir. Bundan kaçındım. Gerçek şu ki, bazen bir yöntemi zaman uyumsuz hale getirilemeyen bir çağrı yığınında kullanmak için senkronize olarak çalıştırmanız gerekir. Senkronizasyon beklemeleri asla yapılmamalıdırsa, o zaman C # ekibinin (eski) bir üyesi olarak, bana neden zaman uyumsuz bir görevi TPL'ye senkronize olarak beklemenin bir değil iki yolunu koyduğunuzu söyleyin.
KeithS

Bu soruyu soran kişi ben değil Stephen Toub olurdu. Sadece şeylerin dil tarafında çalıştım.
Eric Lippert
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.