ProgramingTip

Parallel.ForEach는 대형 개체가있는 열거 형으로 작업하는 경우 "메모리 부족"예외를 유발할 수 있습니다.

bestdevel 2020. 11. 23. 19:44
반응형

Parallel.ForEach는 대형 개체가있는 열거 형으로 작업하는 경우 "메모리 부족"예외를 유발할 수 있습니다.


이미지가 데이터베이스에 데이터베이스를 데이터베이스를 하드 드라이브의 파일에 저장합니다. 이 방법사용 하여 데이터를 쿼리 하여Parallel.ForEach 프로세스 속도를 높이기 위해 사용했습니다 .

그러나 OutOfMemory예외 가 발생한다는 것을 알았습니다 . Parallel.ForEach쿼리를 간격을두기위한 것이 좋습니다 오버 헤드 비용을 지불하기 위해 열거 형 일괄 쿼리를 쿼리 할 것입니다. 하나를 선택합니다 (여러 쿼리를 수행 할 때 마다 소스가 다음 레코드를 메모리에 수행 할 수 있습니다. 더 많이). 밖). 이 문제는 내가 반환하는 레코드 중 하나가 캐싱으로 인해 전체 주소 공간이 사용되는 1-4Mb 바이트 배열 때문입니다 (대상 플랫폼이 32 비트 프로그램은 x86 모드에서 실행되어야합니다. 기계)

캐싱을 만들거나 TPL에 대해 더 작게 만드는 방법이 있습니까?


다음은 문제를 해결하는 프로그램입니다. 이것은 x86 모드에서 되어야만 문제가 오래 걸리거나 시스템에서 발생하지 않는 경우 어레이의 크기가 증가합니다 ( 1 << 20내 시스템에서 약 30 초가 걸리고 4 << 20거의 발생하지 않습니다 ).

class Program
{

    static void Main(string[] args)
    {
        Parallel.ForEach(CreateData(), (data) =>
            {
                data[0] = 1;
            });
    }

    static IEnumerable<byte[]> CreateData()
    {
        while (true)
        {
            yield return new byte[1 << 20]; //1Mb array
        }
    }
}

의 기본 옵션 Parallel.ForEach 은 작업이 CPU 바운드이고 선형 적으로 확장 될 때만 잘 작동합니다 . 작업이 CPU 바운드이면 모든 것이 완벽하게 작동합니다. 쿼드 코어가 있고 실행중인 다른 프로세스가없는 경우 Parallel.ForEach4 개의 프로세서를 모두 사용합니다. 쿼드 코어가 사용하는 컴퓨터의 다른 프로세스가 하나의 전체 CPU를 Parallel.ForEach사용하는 경우 3 개의 프로세서 사용합니다.

그러나 작업이 CPU 바운드가 아닌 경우 작업을 Parallel.ForEach계속 시작하여 모든 CPU를 바쁘게 유지하려고합니다. 그러나 서버는 항상 사용하지 않는 CPU 마력이 더 많지 않은 작업을 계속 생성합니다.

작업이 CPU 바운드인지 어떻게 알 수 있습니까? 바라건대 그것을 검사하는 것입니다. 소수를 인수하면 분명합니다. 그러나 다른 경우는 그렇게 명확하지 않습니다. CPU 바운드인지 확인하는 경험적 방법은 최대 전송 처리 수준을 제한하고 프로그램이 어떻게 작동하는지 관찰하는 것입니다. 작업이 CPU 바운드 인 경우 쿼드 코어 시스템에서 다음과 같은 패턴이 표시되어야합니다.ParallelOptions.MaximumDegreeOfParallelism

  • ParallelOptions.MaximumDegreeOfParallelism = 1: 하나의 전체 CPU 또는 25 % CPU 사용률 사용
  • ParallelOptions.MaximumDegreeOfParallelism = 2: CPU 2 개 또는 CPU 사용률 50 % 사용
  • ParallelOptions.MaximumDegreeOfParallelism = 4: 모든 CPU 사용 또는 100 % CPU 사용률

이와 같이 작동하면 기본 Parallel.ForEach옵션을 사용하여 좋은 결과를 얻을 수 있습니다. 선형 CPU 활용은 좋은 작업을 의미합니다.

그러나 Intel i7에서 샘플 응용 프로그램을 실행하면 최대 전송 처리 수준에 관계없이 CPU 사용률이 약 20 %입니다. 왜 이런거야? 너무 많은 메모리가 할당되어 가비지 수집기가 할당되어 있습니다. 응용 프로그램은 리소스 바인딩되고 리소스는 메모리입니다.

더 많은 데이터베이스 서버에 대해 오래 실행되는 실행되는 I / O 바인딩 작업도 로컬 컴퓨터에서 가능한 모든 CPU 리소스를 사용할 수 없습니다. 그리고 예정된 경우 작업 스케줄러는 새 작업 시작을 "중지 할 때"를 알 수 없습니다.

작업이 CPU 바운드가 아니거나 CPU이 최대 병렬 사용률 처리 수준에 따라 선형 적으로 확장되지 않는 경우 Parallel.ForEach한 번에 너무 많은 작업을 시작하지 않는 것이 좋습니다 . 가장 간단한 방법은 겹치는 I / O 바인딩 작업에 대해 약간의 송신 처리를 허용하는 숫자를 지정하는 것입니다. 그러나 리소스에 대한 로컬 컴퓨터의 요구를 압도하거나 원격 서버에 부담을주지 마십시오. 최상의 결과를 얻을 수있는 시행 착오가 필요합니다.

static void Main(string[] args)
{
    Parallel.ForEach(CreateData(),
        new ParallelOptions { MaxDegreeOfParallelism = 4 },
        (data) =>
            {
                data[0] = 1;
            });
}

릭이 제안한 것은 확실히 중요한 점이지만, 제가 생각하는대로 생각 빠진 또 다른 분할에 대한 논의입니다 .

Parallel::ForEach알려진 길이가없는의 경우 청크 분할 전략 을 사용하는 기본 Partitioner<T>구현을 IEnumerable<T>사용합니다. 이것이 의미하는 바 Parallel::ForEach는 데이터 세트에서 작업하는 데 사용할 각 작업자 스레드 가 몇 개의 요소를 읽은 IEnumerable<T>다음 해당 스레드에 의해서만 처리됩니다 (지금은 작업 도용 무시). 이는 지속적으로 소스로 돌아가서 새로운 작업을 할당하고 다른 작업자 스레드를 위해 일정을 예약해야하는 비용을 절약하기 위해 수행됩니다. 일반적으로 이것은 좋은 일이지만, 특정 시나리오에서 쿼드 코어에 있고 MaxDegreeOfParallelism작업을 위해 4 개의 스레드로 설정 했고 이제 각 스레드에서 100 개의 요소를 가져옵니다.IEnumerable<T>. 글쎄, 그것은 특정 작업자 스레드에 대한 100-400 메가입니다.

그렇다면 이것을 어떻게 해결합니까? 쉽게 사용자 정의 Partitioner<T>구현작성합니다 . 이제 청킹은 귀하의 경우에 여전히 유용하므로 단일 요소 분할 전략을 사용하고 싶지 않을 것입니다. 그러면 필요한 모든 작업 조정에 오버 헤드가 발생하기 때문입니다. 대신 워크로드에 대한 최적의 균형을 찾을 때까지 앱 설정을 통해 조정할 수있는 구성 가능한 버전을 작성합니다. 좋은 소식은 그러한 구현을 작성하는 것이 매우 간단하지만 PFX 팀이 이미 작성하여 병렬 프로그래밍 샘플 프로젝트에 넣었 기 때문에 실제로 직접 작성할 필요가 없다는 것 입니다.


이 문제는 모든 것이 병렬 처리 수준이 아니라 파티 셔 너와 관련이 있습니다. 해결책은 맞춤형 데이터 파티 셔 너를 구현하는 것입니다.

데이터 세트가 크면 TPL의 모노 구현이 메모리 부족을 보장하는 것 같습니다. 이는 최근 저에게 발생했습니다 (본질적으로 위의 루프를 실행하고 있었고 OOM 예외가 발생할 때까지 메모리가 선형 적으로 증가하는 것을 발견했습니다) ).

문제를 추적 한 후 기본적으로 mono가 EnumerablePartitioner 클래스를 사용하여 열거자를 나눕니다. 이 클래스는 작업에 데이터를 제공 할 때마다 계속 증가하는 (그리고 변경할 수없는) 요소 2만큼 데이터를 "청크"한다는 동작이 있습니다. 따라서 태스크가 데이터를 처음 요청할 때 크기의 청크를 얻습니다. 1, 다음 번 크기 2 * 1 = 2, 다음 번 2 * 2 = 4, 2 * 4 = 8 등. 결과는 작업에 전달 된 데이터의 양이됩니다. 메모리가 동시에 증가하고 작업 길이에 따라 증가하며 많은 데이터가 처리되는 경우 필연적으로 메모리 부족 예외가 발생합니다.

아마도이 동작의 원래 이유는 데이터를 얻기 위해 각 스레드가 여러 번 반환되는 것을 피하고 싶지만 처리되는 모든 데이터가 메모리에 맞을 수 있다는 가정을 기반으로하는 것 같습니다. 대용량 파일).

이 문제는 앞에서 설명한대로 사용자 지정 파티 셔 너를 사용하여 피할 수 있습니다. 한 번에 한 항목 씩 각 작업에 데이터를 간단히 반환하는 일반적인 예는 다음과 같습니다.

https://gist.github.com/evolvedmicrobe/7997971

해당 클래스를 먼저 인스턴스화하고 열거 가능 자체 대신 Parallel.For에 전달하십시오.


사용자 지정 파티 셔 너를 사용하는 것이 가장 "정확한"대답이지만 더 간단한 솔루션은 가비지 수집기가 따라 잡도록하는 것입니다. 시도한 경우 함수 내에서 parallel.for 루프를 반복적으로 호출했습니다. 프로그램에서 사용하는 메모리는 여기에 설명 된대로 계속해서 증가했습니다. 나는 추가했다 :

//Force garbage collection.
GC.Collect();
// Wait for all finalizers to complete before continuing.
GC.WaitForPendingFinalizers();

매우 빠르지는 않지만 메모리 문제를 해결했습니다. CPU 사용량과 메모리 사용량이 높으면 가비지 수집기가 효율적으로 작동하지 않을 수 있습니다.

참고 URL : https://stackoverflow.com/questions/6977218/parallel-foreach-can-cause-a-out-of-memory-exception-if-working-with-a-enumera

반응형