Birden çok beklemeden ziyade neden tek 'await Task.WhenAll' tercih etmeliyim?


127

Görevin tamamlanma sırasını önemsemiyorsam ve sadece tamamının tamamlanmasına ihtiyacım olursa await Task.WhenAll, çoklu yerine yine de kullanmalı awaitmıyım? örneğin, DoWork2aşağıdaki DoWork1(ve neden?)

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}

2
Ya paralel olarak kaç tane görev yapmanız gerektiğini gerçekten bilmiyorsanız? Ya 1000 görevin çalıştırılması gerekiyorsa? İlki pek okunamayacak await t1; await t2; ....; await tn=> ikincisi her iki durumda da her zaman en iyi seçimdir
cuongle

Yorumunuz mantıklı. Geçenlerde cevapladığım başka bir soru ile ilgili olarak kendim için bir şeyi açıklamaya çalışıyordum . Bu durumda 3 görev vardı.
2013

Yanıtlar:


113

Evet, WhenAlltüm hataları bir kerede yaydığı için kullanın . Birden çok beklemeyle, daha önceki beklerden biri fırlatırsa hataları kaybedersiniz.

Bir diğer önemli fark, arızalar (hatalı veya iptal edilen görevler) olsa bileWhenAll tüm görevlerin tamamlanmasını bekleyecek olmasıdır . Sırayla manuel olarak beklemek, beklenmedik eşzamanlılığa neden olur çünkü programınızın beklemek isteyen kısmı aslında erken devam edecektir.

Bence bu, kodu okumayı da kolaylaştırıyor çünkü istediğiniz anlambilim doğrudan kodda belgeleniyor.


9
"Çünkü tüm hataları bir kerede yayar" Eğer awaitsonucu olursanız değil .
svick

2
Task ile istisnaların nasıl yönetildiği sorusuna gelince, bu makale arkasındaki mantığa hızlı ama iyi bir fikir veriyor (ve birden çok beklemenin aksine WhenAll'ın faydalarına da geçici bir not veriyor): bloglar .msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg

5
@OskarLindberg OP, ilk görevini beklemeden önce tüm görevlere başlıyor. Böylece eşzamanlı olarak çalışırlar. Bağlantı için teşekkürler.
usr

3
@usr WhenAll'ın semantiğin faydalarını daha da ileri götürmek için aynı SynchronizationContext'i korumak gibi akıllıca şeyler yapıp yapmadığını merak ediyordum. Kesin bir belge bulamadım, ancak IL'ye bakıldığında, IAsyncStateMachine'in oyunda açıkça farklı uygulamaları var. IL'yi o kadar iyi okuyamıyorum, ancak WhenAll en azından daha verimli IL kodu oluşturuyor gibi görünüyor. (Her halükarda, WhenAll'ın sonucunun bana dahil olan tüm görevlerin durumunu yansıtması tek başına çoğu durumda onu tercih etmek için yeterli bir nedendir.)
Oskar Lindberg

17
Bir diğer önemli fark, WhenAll'ın, örneğin t1 veya t2 bir istisna atsa veya iptal edilse bile tüm görevlerin tamamlanmasını bekleyeceğidir.
Magnus

28

Anladığım kadarıyla Task.WhenAllbirden çok awaitURL'yi tercih etmenin ana nedeni performans / görev "çalkalama": DoWork1yöntem şöyle bir şey yapar:

  • belirli bir bağlamla başlayın
  • bağlamı kaydet
  • t1 için bekle
  • orijinal içeriği geri yükle
  • bağlamı kaydet
  • t2 için bekle
  • orijinal içeriği geri yükle
  • bağlamı kaydet
  • t3'ü bekle
  • orijinal içeriği geri yükle

Aksine, şunu DoWork2yapar:

  • belirli bir bağlamla başlayın
  • bağlamı kaydet
  • t1, t2 ve t3'ün tamamını bekleyin
  • orijinal içeriği geri yükle

Bunun sizin özel durumunuz için yeterince büyük bir anlaşma olup olmadığı, elbette "içeriğe bağlıdır" (kelime oyununu bağışlayın).


4
Senkronizasyon bağlamına bir mesaj göndermenin pahalı olduğunu düşünüyorsunuz. Gerçekten değil. Bir kuyruğa eklenen bir temsilciniz var, bu kuyruk okunacak ve temsilci çalıştırılacak. Bunun eklediği ek yük gerçekten çok küçük. Hiçbir şey değil, ama büyük de değil. Eşzamansız işlemler ne olursa olsun masrafı, neredeyse tüm durumlarda bu tür genel giderleri gölgede bırakacaktır.
2013

Kabul edildi, birini diğerine tercih etmenin tek nedeni buydu. Eh, bu artı iş parçacığı değiştirmenin daha önemli bir maliyet olduğu Task.WaitAll ile benzerlik.
Marcel Popescu

1
@Servy Marcel, GERÇEKTEN buna bağlı olduğuna işaret ediyor. Örneğin, prensip olarak tüm db görevlerinde await kullanırsanız ve bu db, asp.net örneğiyle aynı makinede oturursa, bellek içi dizinde, daha ucuz olan bir db isabeti bekleyeceğiniz durumlar vardır. bu senkronizasyon anahtarı ve iş parçacığı karıştırmasından daha fazla. Bu tür bir senaryoda WhenAll () ile önemli bir genel kazanç olabilir, bu yüzden ... gerçekten bağlıdır.
Chris Moschini

3
@ChrisMoschini Sunucu ile aynı makinede oturan bir DB'ye vursa bile, DB sorgusunun, bir mesaj pompasına birkaç delege eklemenin ek yükünden daha hızlı olması mümkün değildir. Bu bellek içi sorgu, neredeyse kesinlikle çok daha yavaş olacak.
57'de hizmet et

Ayrıca t1 daha yavaşsa ve t2 ile t3 daha hızlıysa, diğerinin hemen geri dönmesini beklediğini unutmayın.
David Refaeli

18

Zaman uyumsuz bir yöntem, bir durum makinesi olarak uygulanır. Yöntemleri durum makinelerinde derlenmeyecek şekilde yazmak mümkündür, bu genellikle hızlı izlenen eşzamansız yöntem olarak adlandırılır. Bunlar şu şekilde uygulanabilir:

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

Kullanırken Task.WhenAll, arayan kişinin tüm görevlerin tamamlanmasını beklemesini sağlarken, bu hızlı izleme kodunu korumak mümkündür, örneğin:

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}

7

(Sorumluluk reddi: Bu cevap Ian Griffiths'in Pluralsight'taki TPL Async kursundan alınmıştır / esinlenmiştir )

WhenAll'ı tercih etmenin bir başka nedeni de İstisna işlemedir.

DoWork yöntemlerinde bir dene-yakala bloğunuz olduğunu ve farklı DoTask yöntemlerini çağırdıklarını varsayalım:

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

Bu durumda, 3 görevin tümü istisna atarsa, yalnızca ilk görev yakalanır. Daha sonraki herhangi bir istisna kaybedilecektir. Yani, t2 ve t3 bir istisna atarsa, sadece t2 yakalanacaktır; vb. Sonraki görev istisnaları gözlemlenmeden kalacaktır.

WhenAll'da olduğu gibi - görevlerden herhangi biri veya tümü hatalıysa, ortaya çıkan görev tüm istisnaları içerecektir. Await anahtar sözcüğü her zaman ilk istisnayı yeniden atar. Yani diğer istisnalar hala etkili bir şekilde gözlemlenmemiş durumda. Bunun üstesinden gelmenin bir yolu, WhenAll görevinden sonra boş bir devamlılık eklemek ve beklemeyi oraya koymaktır. Bu şekilde, görev başarısız olursa, result özelliği tam Toplama İstisnasını atar:

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}

6

Bu soruya verilen diğer cevaplar, neden await Task.WhenAll(t1, t2, t3);tercih edildiğine dair teknik nedenler sunar . Bu cevap, hala aynı sonuca varırken, ona daha yumuşak bir taraftan bakmayı (@usr'nin ima ettiği) amaçlayacaktır.

await Task.WhenAll(t1, t2, t3); niyet beyan ettiği ve atomik olduğu için daha işlevsel bir yaklaşımdır.

Bununla birlikte await t1; await t2; await t3;, bir takım arkadaşının (veya belki de gelecekteki kendinizin bile!) Bireysel awaitifadeler arasına kod eklemesini engelleyen hiçbir şey yoktur . Elbette, bunu başarmak için bir satıra sıkıştırdınız, ancak bu sorunu çözmez. Ayrıca, bir ekip ortamında belirli bir kod satırına birden çok ifade eklemek genellikle kötü bir biçimdir çünkü kaynak dosyayı insan gözünün taramasını zorlaştırabilir.

Basitçe ifade etmek gerekirse, await Task.WhenAll(t1, t2, t3);amacınızı daha net bir şekilde ilettiği ve kodda yapılan iyi niyetli güncellemelerden ortaya çıkabilecek veya hatta yanlış giden birleşmelerden doğabilecek tuhaf hatalara karşı daha az savunmasız olduğu için daha sürdürülebilirdir.

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.