Parallel.ForEach vs Task.Factory.StartNew


267

아래 코드 스 니펫의 차이점은 무엇입니까? 둘 다 스레드 풀 스레드를 사용하지 않습니까?

예를 들어 컬렉션의 각 항목에 대해 함수를 호출하려면

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

답변:


302

첫 번째는 훨씬 더 나은 옵션입니다.

Parallel.ForEach는 내부적으로 a Partitioner<T>를 사용 하여 컬렉션을 작업 항목으로 배포합니다. 항목 당 하나의 작업을 수행하지 않고이를 일괄 처리하여 관련 오버 헤드를 줄입니다.

두 번째 옵션은 Task컬렉션의 항목 당 하나의 일정을 예약합니다 . 결과는 (거의) 거의 같지만, 이는 특히 대규모 컬렉션의 경우 필요한 것보다 훨씬 많은 오버 헤드를 발생시키고 전체 런타임이 느려집니다.

참고- 필요한 경우 Parallel.ForEach에 대한 적절한 과부하를 사용하여 사용되는 파티 셔 너를 제어 할 수 있습니다 . 자세한 내용 은 MSDN의 Custom Partitioners 를 참조하십시오 .

런타임시 주요 차이점은 두 번째는 비동기 적으로 작동한다는 것입니다. Parallel.ForEach를 사용하여 다음을 수행하여 복제 할 수 있습니다.

Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));

이렇게해도 여전히 파티 셔 너를 이용하지만 작업이 완료 될 때까지 차단하지 마십시오.


8
Parallel.ForEach에 의해 수행되는 기본 파티셔닝 인 IIRC는 또한 사용 가능한 하드웨어 스레드의 수를 고려하여 최적의 작업 수를 시작하지 않아도됩니다. Microsoft의 병렬 프로그래밍 패턴 기사를 확인하십시오. 여기에이 모든 것들에 대한 훌륭한 설명이 있습니다.
Mal Ross

2
@Mal : 일종의 ... 그것은 실제로 Partitioner가 아니라 TaskScheduler의 일입니다. TaskScheduler는 기본적으로 현재 이것을 잘 처리하는 새로운 ThreadPool을 사용합니다.
리드 콥시

감사. 나는 "나는 전문가가 아니지만 ..."경고에 남겨 두어야한다는 것을 알았습니다. :)
Mal Ross

@ReedCopsey : Parallel.ForEach를 통해 시작된 작업을 래퍼 작업에 연결하는 방법은 무엇입니까? 래퍼 작업에서 .Wait ()을 호출하면 병렬로 실행되는 작업이 완료 될 때까지 중단됩니까?
콘스탄틴 타르 쿠스

1
@Tarkus 여러 요청을하는 경우 각 작업 항목 (병렬 루프)에서 HttpClient.GetString을 사용하는 것이 좋습니다. 일반적으로 이미 동시 루프 안에 비동기 옵션을 넣을 이유가 없습니다.
Reed Copsey

89

"Parallel.For"로 "1,000,000,000 (십억)"번을 실행하고 "Task"객체로 하나를 실행하는 작은 실험을했습니다.

프로세서 시간을 측정하고 Parallel이 더 효율적이라는 것을 알았습니다. 병렬 : 작업을 작은 작업 항목으로 나누고 모든 코어에서 병렬로 최적의 방식으로 실행합니다. 많은 작업 개체를 만드는 동안 (FYI TPL은 내부적으로 스레드 풀링을 사용함) 각 작업에서 모든 실행을 이동하여 아래 실험에서 분명한 상자에 더 많은 스트레스를 만듭니다.

또한 기본 TPL을 설명하는 작은 비디오를 만들었으며 Parallel.For 가 일반적인 작업 및 스레드와 비교하여 http://www.youtube.com/watch?v=No7QqSc5cl8 을 보다 효율적으로 활용하는 방법을 설명했습니다 .

실험 1

Parallel.For(0, 1000000000, x => Method1());

실험 2

for (int i = 0; i < 1000000000; i++)
{
    Task o = new Task(Method1);
    o.Start();
}

프로세서 시간 비교


보다 효율적이며 스레드를 만드는 데 비용이 많이 드는 이유 실험 2는 매우 나쁜 습관입니다.
Tim

@ Georgi- 나쁜 것에 대해 더 많이 이야기하는 것에주의하십시오.
Shivprasad Koirala

3
죄송합니다, 실수입니다. 명확히해야합니다. 1000000000 루프에서 작업을 만드는 것을 의미합니다. 오버 헤드는 상상할 수 없습니다. 물론 Parallel은 한 번에 63 개 이상의 작업을 만들 수 없으므로이 경우 훨씬 더 최적화됩니다.
Georgi-it

이는 1000000000 개의 작업에 해당됩니다. 그러나 이미지 (반복적으로 확대 / 축소 프랙탈)를 처리하고 병렬을 수행하면 마지막 스레드가 완료되기를 기다리는 동안 많은 코어가 유휴 상태입니다. 더 빠르게하기 위해 데이터를 64 개의 작업 패키지로 세분화하고 작업을 만들었습니다. (그런 다음 Task.WaitAll은 완료를 기다립니다.) 아이디어는 1-2 개의 스레드가 (Parallel.For) 할당 된 청크를 완료하기를 기다리는 대신 유휴 스레드가 작업 패키지를 선택하여 작업을 완료하도록하는 것입니다.
Tedd Hansen

1
Mehthod1()이 예에서 무엇을 합니까?
Zapnologica

17

Parallel.ForEach는 루프가 완료 될 때까지 최적화 (새 스레드를 시작하지 않을 수도 있음)하고 차단하며 Task.Factory는 각 항목에 대해 새 작업 인스턴스를 명시 적으로 생성하고 완료되기 전에 반환합니다 (비동기 작업). Parallel.Foreach가 훨씬 더 효율적입니다.


11

내 생각에 가장 현실적인 시나리오는 작업을 완료하기 위해 많은 작업이 필요한 경우입니다. Shivprasad의 접근 방식은 컴퓨팅 자체보다는 객체 생성 / 메모리 할당에 더 중점을 둡니다. 나는 다음과 같은 방법으로 전화를 걸었다.

public static double SumRootN(int root)
{
    double result = 0;
    for (int i = 1; i < 10000000; i++)
        {
            result += Math.Exp(Math.Log(i) / root);
        }
        return result; 
}

이 방법을 실행하는 데 약 0.5 초가 걸립니다.

Parallel을 사용하여 200 번 호출했습니다.

Parallel.For(0, 200, (int i) =>
{
    SumRootN(10);
});

그런 다음 구식 방식으로 200 번 호출했습니다.

List<Task> tasks = new List<Task>() ;
for (int i = 0; i < loopCounter; i++)
{
    Task t = new Task(() => SumRootN(10));
    t.Start();
    tasks.Add(t);
}

Task.WaitAll(tasks.ToArray()); 

첫 번째 사건은 26656ms에 완료되었고 두 번째 사건은 24478ms에 완료되었습니다. 나는 그것을 여러 번 반복했다. 두 번째 접근법은 한계가 빠릅니다.


Parallel.For를 사용하는 것은 구식입니다. 균일하지 않은 작업 단위에는 작업 사용이 권장됩니다. TPL의 Microsoft MVP와 디자이너는 Tasks를 사용하면 스레드를보다 효율적으로 사용할 수 있습니다. 즉, 다른 장치가 완료되기를 기다리는 동안 많은 스레드를 차단하지 않습니다.
Suncat2000
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.