여러 작업에 async / await 사용


406

나는, 각 작업 수익률 중 하나입니다 완전히 asynchrounous 인 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);
}

C # 5 async / await 연산자를 사용하면 여러 작업을 시작하고 모두 완료 될 때까지 기다리는 가장 정확하고 효율적인 방법은 무엇입니까?

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를 사용하고 있기 때문에 5 개의 HTTP 요청을 즉시 발행하여 각 요청이 완료 될 때마다 콘솔에 작성합니다.


그리고 무엇이 문제입니까?
Serg Shevchenko

1
@SergShevchenko 문제는 Parallel.ForEach가 부정확하게 수행된다는 것입니다 (답변 참조)-비동기 코드를 병렬로 실행하려는 시도가 올바른지, 두 가지 솔루션 시도를 제공하는지, 하나가 다른 시도보다 나은지 (아마도 이유가 무엇인지) ).
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)));
}

이를 뒷받침하기 위해 여기에 모든 대안과 그 장점 / 단점을 다루는 자세한 블로그 게시물이 있습니다. ASP.NET 웹 API를 통한 동시 비동기 I / O 방법 및 위치


31
"위의 코드 WaitAll도 스레드를 차단합니다"- 하나의 스레드 만 차단하지는 WaitAll않습니까?
롤링

5
@Rawling 설명서 에는 "Type : System.Threading.Tasks.Task [] 대기 할 Task 인스턴스의 배열"이라고 나와 있습니다. 따라서 모든 스레드를 차단합니다.
Mixxiphoid

30
@Mixxiphoid : 인용 한 비트가 모든 스레드를 차단한다는 의미는 아닙니다. 제공된 작업이 실행되는 동안 호출 스레드 만 차단합니다. 이러한 작업이 실제로 실행되는 방법은 스케줄러에 따라 다릅니다. 일반적으로 각 작업이 완료된 후 실행중인 스레드가 풀로 반환됩니다. 각 스레드는 다른 스레드가 완료 될 때까지 차단 된 상태로 유지되지 않습니다.
musaul

3
@ tugberk, 내가 이해하는 방식으로, "클래식"Task 메소드와 Async 대응 클래스의 유일한 차이점은 태스크가 실행을 시작하고 실행이 끝날 때 스레드와 상호 작용하는 방식입니다. 기본 스케줄러의 클래식 메소드는 해당 기간 동안 ( "잠자기"상태 인 경우에도) 스레드를 비우지 만 비동기 메소드는 그렇지 않습니다. 해당 기간 이외의 차이는 없습니다. 즉, 작업이 예약되었지만 시작되지 않았으며 완료되었지만 호출자가 여전히 대기 중입니다.
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을 실행하는 것은 약간 이상합니다. 이 경우에는 중요하지 않지만 Task에 더 의미가 없습니다. 계산 작업을 시뮬레이트하는 경우 작업자 스레드를 실행하거나 I / O를 시뮬레이트하는 경우 절전 대신 Task.Delay를 실행합니까? 당신의 생각이 무엇인지 확인하십시오.
아노 자켄

24

호출하는 API는 비동기이므로 Parallel.ForEach 버전이 의미가 없습니다. 당신은 사용 야해 .WaitWaitAll그 호출자가 비동기 사용하는 경우 병렬에게 또 다른 대안을 잃게 이후 버전 Task.WhenAll수행 한 후 SelectToArray작업의 배열을 생성 할 수 있습니다. 두 번째 대안은 Rx 2.0을 사용하는 것입니다


10

n 개의 작업을 Task.WhenAll전달할 수있는 기능을 사용할 수 있습니다 . 전달한 모든 작업을 완료하면 완료된 작업을 반환합니다 . UI 스레드를 차단하지 않으려면 비동기식으로 기다려야 합니다.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.ForEach각 작업자와 함께 수행 하려면 사용자 정의 작업자 목록 과 비동기 Action 가 필요합니다.

Task.WaitAllTask.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.