Birden çok görev için zaman uyumsuz / beklemeyi kullanma


406

Tamamen eşzamansız bir API istemcisi kullanıyorum, yani, her işlem ya döndürür Taskya da Task<T>, örneğin:

static async Task DoSomething(int siteId, int postId, IBlogClient client)
{
    await client.DeletePost(siteId, postId); // call API client
    Console.WriteLine("Deleted post {0}.", siteId);
}

C # 5 zaman uyumsuz / bekleyen işleçlerini kullanarak, birden çok görevi başlatmak ve bunların tamamlanmasını beklemek için en doğru / en etkili yol nedir:

int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());

veya:

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());

API istemcisi HttpClient'i dahili olarak kullandığından, her biri tamamlandığında konsola yazarak hemen 5 HTTP isteği vermesini beklerim.


Sorun ne?
Serg Shevchenko

1
@SergShevchenko Sorun şu ki, Parallel.ForEach yanlış yapılır (cevaplara bakın) - async kodunu paralel olarak çalıştırma girişimlerinin doğru olup olmadığını, iki çözüm denemesi sunmasını ve birinin diğerinden daha iyi olup olmadığını soruyor (ve muhtemelen neden böyle olduğunu soruyor) ).
AnorZaken

Yanıtlar:


572
int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());

İşlemleri yukarıdaki koda paralel olarak çalıştırsanız da, bu kod her işlemin üzerinde çalıştığı her iş parçacığını engeller. Örneğin, şebeke çağrısı 2 saniye sürüyorsa, her iş parçacığı beklemekten başka bir şey yapmadan 2 saniye bekler.

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());

Öte yandan, yukarıdaki kod WaitAllda iş parçacığı engeller ve iş parçacığı işlem bitene kadar başka bir iş işlemek için ücretsiz olmayacaktır.

Önerilen Yaklaşım

WhenAllParalel olarak işlemlerinizi eşzamansız olarak gerçekleştirmesini tercih ederim .

public async Task DoWork() {

    int[] ids = new[] { 1, 2, 3, 4, 5 };
    await Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}

Aslında, yukarıdaki durumda, ihtiyacınız bile awaityoktur, herhangi bir sürekliliğiniz olmadığı için doğrudan yöntemden geri dönebilirsiniz:

public Task DoWork() 
{
    int[] ids = new[] { 1, 2, 3, 4, 5 };
    return Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}

Bunu yedeklemek için, tüm alternatifleri ve avantajlarını / dezavantajlarını inceleyen ayrıntılı bir blog yazısı: ASP.NET Web API ile Eşzamanlı Asenkron I / O Nasıl ve Nerede


31
"ile yukarıdaki kodu WaitAllda iş parçacığı engeller" - sadece bir iş parçacığı, değil de bir iş parçacığı engellemek WaitAllmi?
Rawling

5
@ Belgeleri düzenleme , "Tür: System.Threading.Tasks.Task [] Beklenecek bir Görev örnekleri dizisi" olduğunu belirtir. Böylece tüm iplikleri engeller.
Mixxiphoid

30
@Mixxiphoid: Alıntıladığınız bit, tüm konuları engellediği anlamına gelmez. Sağlanan görevler çalışırken yalnızca çağrı dizisini engeller. Bu görevlerin gerçekte nasıl yürütüldüğü, zamanlayıcıya bağlıdır. Genellikle her görev tamamlandıktan sonra üzerinde çalıştığı iş parçacığı havuza geri döner. Diğerleri tamamlanana kadar her iş parçacığı engellenmez.
musaul

3
@tugberk, Anladığım şekilde, "klasik" Görev yöntemleri ile Async meslektaşları arasındaki tek fark, bir görevin çalışmaya başlamasıyla çalışmayı bitirmesi arasındaki iş parçacıklarıyla nasıl etkileşimde bulunduklarıdır. Varsayılan bir zamanlayıcı altındaki klasik yöntem, o zaman zarfında ("uyku" olsa bile) bir iş parçacığını domuzcuğa alırken, zaman uyumsuz olanlar bunu yapmayacaktır. Bu sürenin dışında hiçbir fark yoktur, yani görev zamanlanmış ancak başlatılmamıştır ve tamamlandığında ancak arayan hala beklemektedir.
musaul

3
@tugberk Bkz. stackoverflow.com/a/6123432/750216 Arayan iş parçacığının engellenip engellenmemesi arasındaki farktır, geri kalanı aynıdır. Açıklığa kavuşturmak için cevabı düzenlemek isteyebilirsiniz.
Răzvan Flavius ​​Panda

45

Soruda verilen yöntemlerin sonuçlarını ve kabul edilen cevabı görmeyi merak ettim, bu yüzden test ettim.

İşte kod:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTest
{
    class Program
    {
        class Worker
        {
            public int Id;
            public int SleepTimeout;

            public async Task DoWork(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart-testStart).TotalSeconds.ToString("F2"));
                await Task.Run(() => Thread.Sleep(SleepTimeout));
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd-workerStart).TotalSeconds.ToString("F2"), (workerEnd-testStart).TotalSeconds.ToString("F2"));
            }
        }

        static void Main(string[] args)
        {
            var workers = new List<Worker>
            {
                new Worker { Id = 1, SleepTimeout = 1000 },
                new Worker { Id = 2, SleepTimeout = 2000 },
                new Worker { Id = 3, SleepTimeout = 3000 },
                new Worker { Id = 4, SleepTimeout = 4000 },
                new Worker { Id = 5, SleepTimeout = 5000 },
            };

            var startTime = DateTime.Now;
            Console.WriteLine("Starting test: Parallel.ForEach...");
            PerformTest_ParallelForEach(workers, startTime);
            var endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WaitAll...");
            PerformTest_TaskWaitAll(workers, startTime);
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WhenAll...");
            var task = PerformTest_TaskWhenAll(workers, startTime);
            task.Wait();
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            Console.ReadKey();
        }

        static void PerformTest_ParallelForEach(List<Worker> workers, DateTime testStart)
        {
            Parallel.ForEach(workers, worker => worker.DoWork(testStart).Wait());
        }

        static void PerformTest_TaskWaitAll(List<Worker> workers, DateTime testStart)
        {
            Task.WaitAll(workers.Select(worker => worker.DoWork(testStart)).ToArray());
        }

        static Task PerformTest_TaskWhenAll(List<Worker> workers, DateTime testStart)
        {
            return Task.WhenAll(workers.Select(worker => worker.DoWork(testStart)));
        }
    }
}

Ve ortaya çıkan çıktı:

Starting test: Parallel.ForEach...
Worker 1 started on thread 1, beginning 0.21 seconds after test start.
Worker 4 started on thread 5, beginning 0.21 seconds after test start.
Worker 2 started on thread 3, beginning 0.21 seconds after test start.
Worker 5 started on thread 6, beginning 0.21 seconds after test start.
Worker 3 started on thread 4, beginning 0.21 seconds after test start.
Worker 1 stopped; the worker took 1.90 seconds, and it finished 2.11 seconds after the test start.
Worker 2 stopped; the worker took 3.89 seconds, and it finished 4.10 seconds after the test start.
Worker 3 stopped; the worker took 5.89 seconds, and it finished 6.10 seconds after the test start.
Worker 4 stopped; the worker took 5.90 seconds, and it finished 6.11 seconds after the test start.
Worker 5 stopped; the worker took 8.89 seconds, and it finished 9.10 seconds after the test start.
Test finished after 9.10 seconds.

Starting test: Task.WaitAll...
Worker 1 started on thread 1, beginning 0.01 seconds after test start.
Worker 2 started on thread 1, beginning 0.01 seconds after test start.
Worker 3 started on thread 1, beginning 0.01 seconds after test start.
Worker 4 started on thread 1, beginning 0.01 seconds after test start.
Worker 5 started on thread 1, beginning 0.01 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.01 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.01 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.01 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.01 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.

Starting test: Task.WhenAll...
Worker 1 started on thread 1, beginning 0.00 seconds after test start.
Worker 2 started on thread 1, beginning 0.00 seconds after test start.
Worker 3 started on thread 1, beginning 0.00 seconds after test start.
Worker 4 started on thread 1, beginning 0.00 seconds after test start.
Worker 5 started on thread 1, beginning 0.00 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.00 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.00 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.00 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.00 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.00 seconds after the test start.
Test finished after 5.00 seconds.

2
Bu sonuçların her birine zaman
ayırırsanız

8
@SerjSagan ilk fikrim sadece işçilerin her seferinde aynı anda başlatıldığını doğrulamaktı, ancak testin netliğini artırmak için zaman damgaları ekledim. Önerin için teşekkürler.
RiaanDP

Test için teşekkürler. Ancak "işçi iş parçacığından" ayrı bir iş parçacığında thread.sleep çalıştırdığınız biraz garip geliyor. Bu durumda önemli değil, ama Görev için daha mantıklı olmaz. Hesaplama işini simüle edersek işçi iş parçacıklarını çalıştırın veya i / o simüle edersek uyku yerine sadece Görev. Sadece düşüncelerinizin ne olacağını kontrol edin.
AnorZaken

24

Aradığınız API zaman uyumsuz olduğundan Parallel.ForEachsürüm pek bir anlam ifade etmiyor. Sen kullanılmamalı .WaitiçindeWaitAll o arayan zaman uyumsuz kullandığı takdirde paralellik başka alternatifi kaybedecek beri sürümü Task.WhenAllyaptıktan sonra Selectve ToArraygörevlerin dizisini oluşturmak için. İkinci bir alternatif Rx 2.0 kullanıyor


10

N görev Task.WhenAlliletebileceğiniz işlevi kullanabilirsiniz ; tamamladığınız tüm görevler tamamlandığında çalışacak bir görevi döndürür . Kullanıcı arayüzü iş parçacığınızı engellememeniz için zaman uyumsuz olarak beklemeniz gerekir :Task.WhenAllTask.WhenAllTask.WhenAll

   public async Task DoSomeThing() {

       var Task[] tasks = new Task[numTasks];
       for(int i = 0; i < numTask; i++)
       {
          tasks[i] = CallSomeAsync();
       }
       await Task.WhenAll(tasks);
       // code that'll execute on UI thread
   }

8

Parallel.ForEachkullanıcı tanımlı çalışanların bir listesini ve zaman uyumsuz bir Action her çalışanla çalışmak için zaman gerektirir.

Task.WaitAllve Task.WhenAllbir ihtiyaç List<Task>tanımı asenkron göre olan.

Buldum RiaanDP bireyin tepkisi farkı anlamak için çok yararlı, ancak bir düzeltme ihtiyacı Parallel.ForEach. Yorumuna cevap vermek için yeterli itibar, dolayısıyla kendi cevabım.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTest
{
    class Program
    {
        class Worker
        {
            public int Id;
            public int SleepTimeout;

            public void DoWork(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds.ToString("F2"));
                Thread.Sleep(SleepTimeout);
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd - workerStart).TotalSeconds.ToString("F2"), (workerEnd - testStart).TotalSeconds.ToString("F2"));
            }

            public async Task DoWorkAsync(DateTime testStart)
            {
                var workerStart = DateTime.Now;
                Console.WriteLine("Worker {0} started on thread {1}, beginning {2} seconds after test start.",
                    Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds.ToString("F2"));
                await Task.Run(() => Thread.Sleep(SleepTimeout));
                var workerEnd = DateTime.Now;
                Console.WriteLine("Worker {0} stopped; the worker took {1} seconds, and it finished {2} seconds after the test start.",
                   Id, (workerEnd - workerStart).TotalSeconds.ToString("F2"), (workerEnd - testStart).TotalSeconds.ToString("F2"));
            }
        }

        static void Main(string[] args)
        {
            var workers = new List<Worker>
            {
                new Worker { Id = 1, SleepTimeout = 1000 },
                new Worker { Id = 2, SleepTimeout = 2000 },
                new Worker { Id = 3, SleepTimeout = 3000 },
                new Worker { Id = 4, SleepTimeout = 4000 },
                new Worker { Id = 5, SleepTimeout = 5000 },
            };

            var startTime = DateTime.Now;
            Console.WriteLine("Starting test: Parallel.ForEach...");
            PerformTest_ParallelForEach(workers, startTime);
            var endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WaitAll...");
            PerformTest_TaskWaitAll(workers, startTime);
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            startTime = DateTime.Now;
            Console.WriteLine("Starting test: Task.WhenAll...");
            var task = PerformTest_TaskWhenAll(workers, startTime);
            task.Wait();
            endTime = DateTime.Now;
            Console.WriteLine("Test finished after {0} seconds.\n",
                (endTime - startTime).TotalSeconds.ToString("F2"));

            Console.ReadKey();
        }

        static void PerformTest_ParallelForEach(List<Worker> workers, DateTime testStart)
        {
            Parallel.ForEach(workers, worker => worker.DoWork(testStart));
        }

        static void PerformTest_TaskWaitAll(List<Worker> workers, DateTime testStart)
        {
            Task.WaitAll(workers.Select(worker => worker.DoWorkAsync(testStart)).ToArray());
        }

        static Task PerformTest_TaskWhenAll(List<Worker> workers, DateTime testStart)
        {
            return Task.WhenAll(workers.Select(worker => worker.DoWorkAsync(testStart)));
        }
    }
}

Ortaya çıkan çıktı aşağıdadır. Yürütme süreleri karşılaştırılabilir. Bilgisayarım haftalık antivirüs taraması yaparken bu testi yaptım. Testlerin sırasını değiştirmek, testlerin yürütme sürelerini değiştirdi.

Starting test: Parallel.ForEach...
Worker 1 started on thread 9, beginning 0.02 seconds after test start.
Worker 2 started on thread 10, beginning 0.02 seconds after test start.
Worker 3 started on thread 11, beginning 0.02 seconds after test start.
Worker 4 started on thread 13, beginning 0.03 seconds after test start.
Worker 5 started on thread 14, beginning 0.03 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.02 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.02 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.03 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.03 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.03 seconds after the test start.
Test finished after 5.03 seconds.

Starting test: Task.WaitAll...
Worker 1 started on thread 9, beginning 0.00 seconds after test start.
Worker 2 started on thread 9, beginning 0.00 seconds after test start.
Worker 3 started on thread 9, beginning 0.00 seconds after test start.
Worker 4 started on thread 9, beginning 0.00 seconds after test start.
Worker 5 started on thread 9, beginning 0.01 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.01 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.01 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.01 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.01 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.

Starting test: Task.WhenAll...
Worker 1 started on thread 9, beginning 0.00 seconds after test start.
Worker 2 started on thread 9, beginning 0.00 seconds after test start.
Worker 3 started on thread 9, beginning 0.00 seconds after test start.
Worker 4 started on thread 9, beginning 0.00 seconds after test start.
Worker 5 started on thread 9, beginning 0.00 seconds after test start.
Worker 1 stopped; the worker took 1.00 seconds, and it finished 1.00 seconds after the test start.
Worker 2 stopped; the worker took 2.00 seconds, and it finished 2.00 seconds after the test start.
Worker 3 stopped; the worker took 3.00 seconds, and it finished 3.00 seconds after the test start.
Worker 4 stopped; the worker took 4.00 seconds, and it finished 4.00 seconds after the test start.
Worker 5 stopped; the worker took 5.00 seconds, and it finished 5.01 seconds after the test start.
Test finished after 5.01 seconds.
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.