ใช้ async / รอหลายงาน


406

ฉันใช้ไคลเอนต์ API ที่ไม่สมบูรณ์อย่างสมบูรณ์นั่นคือการดำเนินการแต่ละอย่างกลับมาTaskหรือTask<T>เช่น:

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

การใช้ตัวดำเนินการ async / await C # 5 วิธีที่ถูกต้อง / มีประสิทธิภาพมากที่สุดในการเริ่มงานหลาย ๆ อย่างและรอให้พวกเขาทั้งหมดดำเนินการให้เสร็จสิ้นคืออะไร:

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

หรือ:

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

เนื่องจากไคลเอนต์ API กำลังใช้ HttpClient ภายในฉันคาดว่าสิ่งนี้จะส่งคำขอ HTTP 5 รายการทันทีโดยเขียนไปยังคอนโซลเมื่อแต่ละรายการเสร็จสมบูรณ์


แล้วปัญหาคืออะไร?
Serg Shevchenko

1
@SergShevchenko ปัญหาก็คือ Parallel ของเขา ForEach นั้นทำอย่างไม่ถูกต้อง (ดูคำตอบ) - เขาถามว่าความพยายามของเขาในการรัน async โค๊ดในแบบขนานนั้นถูกต้องหรือไม่และมีวิธีที่ดีกว่าอีกวิธีหนึ่ง )
AnorZaken

คำตอบ:


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

แม้ว่าคุณจะเรียกใช้การดำเนินการควบคู่กับรหัสด้านบนรหัสนี้จะบล็อกแต่ละเธรดที่การดำเนินการแต่ละการทำงาน ตัวอย่างเช่นหากการโทรเครือข่ายใช้เวลา 2 วินาทีแต่ละเธรดจะค้างเป็นเวลา 2 วินาทีโดยไม่ต้องทำอะไรนอกจากรอ

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

ในทางกลับกันโค้ดด้านบนที่มีการWaitAllบล็อกเธรดและเธรดของคุณจะไม่สามารถดำเนินการงานอื่น ๆ ได้จนกว่าการดำเนินการจะสิ้นสุดลง

แนวทางที่แนะนำ

ฉันต้องการWhenAllที่จะดำเนินการของคุณแบบอะซิงโครนัสในแบบขนาน

public async Task DoWork() {

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

ในความเป็นจริงในกรณีข้างต้นคุณไม่จำเป็นต้องทำawaitคุณสามารถกลับจากวิธีการโดยตรงเนื่องจากคุณไม่มีการดำเนินการใด ๆ :

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

ในการสำรองข้อมูลนี่คือโพสต์บล็อกที่มีรายละเอียดซึ่งจะผ่านตัวเลือกทั้งหมดและข้อดี / ข้อเสียของพวกเขา: อย่างไรและที่ไหนพร้อมกัน Asynchronous I / O พร้อม ASP.NET Web API


31
"โค้ดด้านบนที่มีการWaitAllบล็อกเธรดด้วย" - ไม่เพียง แต่บล็อกหนึ่งเธรดที่เรียกว่าWaitAllใช่หรือไม่
Rawling

5
@Rawling เอกสารระบุว่า "Type: System.Threading.Tasks.Task [] อาร์เรย์ของอินสแตนซ์งานที่จะต้องรอ" ดังนั้นจึงบล็อกเธรดทั้งหมด
Mixxiphoid

30
@Mixxiphoid: บิตที่คุณยกมาไม่ได้หมายความว่ามันบล็อกเธรดทั้งหมด มันบล็อกเฉพาะเธรดการโทรในขณะที่งานที่ให้มากำลังทำงานอยู่ วิธีที่งานเหล่านั้นทำงานจริงขึ้นอยู่กับตัวกำหนดตารางเวลา โดยทั่วไปหลังจากแต่ละงานเสร็จสมบูรณ์เธรดที่กำลังทำงานอยู่จะได้รับการส่งคืนไปยังกลุ่ม แต่ละเธรดจะไม่ถูกบล็อกจนกว่าจะเสร็จสมบูรณ์
musaul

3
@tugberk วิธีที่ฉันเข้าใจความแตกต่างเพียงอย่างเดียวระหว่างวิธีการ "คลาสสิค" และ Async counterparts คือวิธีที่พวกเขาโต้ตอบกับเธรดระหว่างเมื่องานเริ่มทำงานและเสร็จสิ้นการทำงาน วิธีคลาสสิกภายใต้ตัวกำหนดตารางเวลาเริ่มต้นจะทำให้เธรดในช่วงเวลานั้น (แม้ว่าจะเป็น "กำลังหลับ") ในขณะที่อะซิงก์จะไม่ทำงาน ไม่แตกต่างจากช่วงเวลาดังกล่าวนั่นคืองานเป็นกำหนดการ แต่ยังไม่ได้เริ่มและเมื่องานเสร็จ แต่ผู้โทรยังรออยู่
musaul

3
@tugberk ดูstackoverflow.com/a/6123432/750216ความแตกต่างอยู่ที่ว่าเธรดการโทรถูกบล็อกหรือไม่ส่วนที่เหลือเหมือนกัน คุณอาจต้องการแก้ไขคำตอบเพื่อชี้แจง
Răzvan Flavius ​​Panda

45

ฉันอยากรู้ว่าจะเห็นผลลัพธ์ของวิธีการที่ให้ไว้ในคำถามเช่นเดียวกับคำตอบที่ยอมรับได้ฉันจึงนำไปทดสอบ

นี่คือรหัส:

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

และผลลัพธ์ที่ได้:

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
หากคุณให้เวลากับผลลัพธ์แต่ละรายการสิ่งนี้จะมีประโยชน์มากกว่า
Serj Sagan

8
@SerjSagan ความคิดเริ่มต้นของฉันคือการตรวจสอบว่าคนงานกำลังเริ่มพร้อมกันในแต่ละกรณี แต่ฉันได้เพิ่มการประทับเวลาเพื่อปรับปรุงความชัดเจนของการทดสอบ ขอบคุณสำหรับคำแนะนำ
RiaanDP

ขอบคุณสำหรับการทดสอบ อย่างไรก็ตามรู้สึกแปลกเล็กน้อยที่คุณใช้งาน thread.sleep บนเธรดแยกจาก "thread ผู้ทำงาน" ไม่ใช่ว่ามันมีความสำคัญในกรณีนี้ แต่มันจะไม่มีเหตุผลที่ดีกว่าสำหรับ Task.Run เธรดผู้ปฏิบัติงานถ้าเราจำลองการทำงานของคอมพิวเตอร์หรือเพียงแค่ Task.Delay แทนการนอนหลับถ้าเราจำลอง i / o? แค่ตรวจสอบความคิดของคุณว่าจะเป็นอย่างไร
AnorZaken

24

เนื่องจาก API ที่คุณโทรหานั้นเป็นแบบอะซิงโครนัสParallel.ForEachเวอร์ชันจึงไม่สมเหตุสมผลนัก คุณไม่ควรใช้.WaitในWaitAllรุ่นตั้งแต่ที่จะสูญเสียความเท่าเทียมกันอีกทางเลือกหนึ่งถ้าโทรเป็น async ใช้Task.WhenAllหลังจากทำSelectและToArrayเพื่อสร้างอาร์เรย์ของงาน ทางเลือกที่สองคือการใช้ Rx 2.0


10

คุณสามารถใช้Task.WhenAllฟังก์ชั่นที่คุณสามารถผ่านงานn ; Task.WhenAllจะส่งคืนงานที่จะเสร็จสมบูรณ์เมื่องานทั้งหมดที่คุณส่งให้Task.WhenAllเสร็จสมบูรณ์ คุณต้องรอแบบอะซิงโครนัสTask.WhenAllเพื่อไม่ให้บล็อกเธรด UI ของคุณ:

   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.ForEachต้องการรายชื่อของผู้ใช้งานที่กำหนดโดยผู้ใช้และไม่ใช่ async Actionเพื่อดำเนินการกับผู้ปฏิบัติงานแต่ละคน

Task.WaitAllและTask.WhenAllต้องมีList<Task> , ซึ่งนิยามโดยอะซิงโครนัส

ผมพบว่าRiaanDPของการตอบสนองที่มีประโยชน์มากที่จะเข้าใจความแตกต่าง Parallel.ForEachแต่ต้องแก้ไขเป็น ชื่อเสียงไม่เพียงพอที่จะตอบความคิดเห็นของเขาดังนั้นการตอบสนองของฉันเอง

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

ผลลัพธ์ที่ได้คือด้านล่าง เวลาดำเนินการเทียบเคียง ฉันรันการทดสอบนี้ในขณะที่คอมพิวเตอร์กำลังทำการสแกนไวรัสทุกสัปดาห์ การเปลี่ยนลำดับของการทดสอบได้เปลี่ยนเวลาดำเนินการของพวกเขา

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.
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.