Menggunakan async / menunggu untuk beberapa tugas

406

Saya menggunakan klien API yang sepenuhnya asynchrounous, yaitu setiap operasi baik kembali Taskatau Task<T>, misalnya:

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

Dengan menggunakan operator async / await C # 5, apa cara yang benar / paling efisien untuk memulai banyak tugas dan menunggu semuanya selesai:

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

atau:

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

Karena klien API menggunakan HttpClient secara internal, saya berharap ini akan segera mengeluarkan 5 permintaan HTTP, menulis ke konsol setelah masing-masing selesai.

Ben Foster
sumber
Dan apa masalahnya?
Serg Shevchenko
1
@SergShevchenko Masalahnya adalah Paralelnya. ForEach dilakukan dengan tidak benar (lihat jawaban) - dia bertanya apakah upayanya untuk menjalankan kode async secara paralel sudah benar, menawarkan dua upaya solusi, dan jika satu lebih baik dari yang lain (dan mungkin mengapa demikian ).
AnorZaken

Jawaban:

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

Meskipun Anda menjalankan operasi secara paralel dengan kode di atas, kode ini memblokir setiap utas yang menjalankan setiap operasi. Misalnya, jika panggilan jaringan membutuhkan waktu 2 detik, setiap utas hang selama 2 detik tanpa melakukan apa-apa selain menunggu.

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

Di sisi lain, kode di atas dengan WaitAlljuga memblokir utas dan utas Anda tidak akan bebas untuk memproses pekerjaan lain sampai operasi berakhir.

Pendekatan yang Disarankan

Saya lebih suka WhenAllyang akan melakukan operasi Anda secara sinkron secara paralel.

public async Task DoWork() {

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

Bahkan, dalam kasus di atas, Anda bahkan tidak perlu await, Anda bisa langsung kembali dari metode karena Anda tidak memiliki kelanjutan:

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

Untuk mendukung hal ini, berikut adalah posting blog terperinci yang membahas semua alternatif dan keuntungan / kerugiannya: Bagaimana dan Di mana Concurrent I / O Asinkron dengan API Web ASP.NET

tugberk
sumber
31
"kode di atas dengan WaitAlljuga memblokir utas" - bukan hanya memblokir satu utas, yang disebut WaitAll?
Rawling
5
@Melacak dokumentasi menyatakan bahwa "Ketik: System.Threading.Tasks.Task [] Array dari instance Task yang akan ditunggu.". Jadi, itu memblokir semua utas.
Mixxiphoid
30
@Mixxiphoid: Bit yang Anda kutip tidak berarti bahwa ia memblokir semua utas. Ini hanya memblokir utas panggilan saat tugas yang disediakan sedang berjalan. Bagaimana tugas-tugas itu benar-benar dijalankan, tergantung pada penjadwal. Biasanya setelah setiap tugas selesai, utas yang dijalankan akan kembali ke kolam. Setiap utas tidak akan tetap diblokir sampai yang lain selesai.
musaul
3
@tugberk, Cara saya memahaminya, satu-satunya perbedaan antara metode tugas "klasik" dan rekan Async adalah bagaimana mereka berinteraksi dengan utas antara saat tugas mulai berjalan dan selesai berjalan. Metode klasik di bawah penjadwal default akan menanggung utas selama periode itu (bahkan jika itu "tidur"), sedangkan yang async tidak akan. Tidak ada perbedaan di luar periode itu, yaitu tugas adalah jadwal tetapi tidak dimulai, dan ketika telah selesai tetapi peneleponnya masih menunggu.
musaul
3
@tugberk Lihat stackoverflow.com/a/6123432/750216 perbedaannya terletak pada apakah utas panggilan diblokir atau tidak, sisanya sama. Anda mungkin ingin mengedit jawaban untuk menjelaskan.
Răzvan Flavius ​​Panda
45

Saya ingin melihat hasil dari metode yang disediakan dalam pertanyaan serta jawaban yang diterima, jadi saya mengujinya.

Berikut kodenya:

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)));
        }
    }
}

Dan output yang dihasilkan:

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.
RiaanDP
sumber
2
Jika Anda memberi waktu pada masing-masing hasil ini, ini akan lebih bermanfaat
Serj Sagan
8
@SerjSagan ide awal saya hanya untuk memverifikasi bahwa para pekerja mulai bersamaan dalam setiap kasus, tetapi saya telah menambahkan perangko waktu untuk meningkatkan kejelasan tes. Terima kasih untuk sarannya.
RiaanDP
Terima kasih untuk tesnya. Namun rasanya agak aneh bahwa Anda menjalankan utas. Tidur di utas terpisah dari "utas pekerja". Bukan berarti itu penting dalam kasus ini, tetapi tidakkah itu lebih masuk akal untuk Task. Jalankan utas pekerja jika kita mensimulasikan pekerjaan komputasi, atau hanya Task. Hanya memeriksa apa pendapat Anda tentang hal itu.
AnorZaken
24

Karena API yang Anda panggil adalah async, Parallel.ForEachversi itu tidak masuk akal. Anda tidak boleh menggunakan .Waitdalam WaitAllversi karena itu akan kehilangan paralelisme Alternatif lain jika pemanggil adalah async gunakan Task.WhenAllsetelah melakukan Selectdan ToArrayuntuk menghasilkan berbagai tugas. Alternatif kedua menggunakan Rx 2.0

James Manning
sumber
10

Anda dapat menggunakan Task.WhenAllfungsi yang dapat Anda lewati dan tugas; Task.WhenAllakan mengembalikan tugas yang selesai saat semua tugas yang Anda Task.WhenAllselesaikan selesai. Anda harus menunggu secara tidak sinkron Task.WhenAllagar Anda tidak akan memblokir utas UI Anda:

   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
   }
Ahmed Wasim
sumber
8

Parallel.ForEachmembutuhkan daftar pekerja yang ditentukan pengguna dan non-async Action untuk melakukan dengan masing-masing pekerja.

Task.WaitAlldan Task.WhenAllmemerlukan List<Task>, yang menurut definisi tidak sinkron.

Saya menemukan RiaanDP 's respon sangat berguna untuk memahami perbedaan, tapi perlu koreksi untuk Parallel.ForEach. Reputasi tidak cukup untuk menanggapi komentarnya, demikian tanggapan saya sendiri.

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)));
        }
    }
}

Output yang dihasilkan di bawah ini. Waktu eksekusi dapat dibandingkan. Saya menjalankan tes ini ketika komputer saya melakukan pemindaian anti virus mingguan. Mengubah urutan pengujian memang mengubah waktu pelaksanaannya.

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.
JPortillo
sumber