[C#] 병렬 프로그래밍 실습: 병렬 프로그래밍의 패턴

      이 장에서는 병렬 프로그래밍 패턴을   소개하고 병렬 코드 문제 시나리오를 이해하고 병렬 프로그래밍/비동기 기술을 사용하여 문제를 해결하는 데 중점을 둡니다. 이 장에서는 가장 중요한 프로그래밍 패턴 중 일부를 소개합니다.

        이 튜토리얼로 엔지니어링 배우기: Magician Dix/HandsOnParallelProgramming · GitCode


1. 맵리듀스 모드

        맵리듀스는 서버 전반에 걸친 대규모 컴퓨팅 요구사항 등 빅데이터 처리 문제를 해결하기 위해 도입됐다. 이 모드는 단일 코어 컴퓨터에서 사용할 수 있습니다.

1.1 매핑과 축소

        MapReduce 프로그램은 이름에서 알 수 있듯이 Map + Reduce입니다. MapReduce 프로그램에 대한 입력은 키-값 쌍으로 전달되며 출력은 동일한 형식입니다.

        책에서 말하는 내용은 매우 추상적으로 들리므로 이해를 돕기 위해 그림을 그려 보십시오.

         목록을 입력한 다음 어떤 방식으로든 필터링(목록 반환)한 다음 그룹화(키 값 반환)하고 마지막으로 각 그룹의 키-값 쌍을 결과로 반환합니다.

1.2 LINQ를 사용하여 MapReduce 구현

        해당 예에서 확장 방법은 다음과 같습니다.

public static ParallelQuery<TResult> MapReduce<TSource, TMapped, TKey, TResult>(
            this ParallelQuery<TSource> source,
            Func<TSource, IEnumerable<TMapped>> map,
            Func<TMapped, TKey> keySelector,
            Func<IGrouping<TKey, TMapped>, IEnumerable<TResult>> reduce)
        {
            return source.SelectMany(map)
                .GroupBy(keySelector)
                .SelectMany(reduce);
        }

        이 기능을 이해하기 위해 요구사항을 사용합니다.

  1. 소스 데이터는 -100~100 범위의 난수 1000개입니다.

  2. 양수를 필터링합니다.

  3. 10자리 숫자에 따라 그룹화합니다(0에서 9까지의 그룹 하나, 10에서 19까지의 그룹 하나 등).

  4. 각 그룹의 수를 센다.

        그런 다음 위의 MapReduce 템플릿을 사용하여 처리합니다. 샘플 코드는 다음과 같습니다.

        private void RunMapReduce()
        {
            //初始化原始数据
            int length = 1000;
            List<int> L = new List<int>(length);
            for (int i = 0; i < length; i++)
            {
                L.Add(Random.Range(-100, 100));
            }

            var ret = L.AsParallel().MapReduce(
                mapPositiveNumbers,//筛选正数
                groupNumbers,//映射分组
                reduceNumbers);//归约合并结果

            foreach (var item in ret)
            {
                Debug.Log($"{item.Key * 10} ~ {(item.Key + 1) * 10} 出现了:{item.Value} 次 !");
            }
        }
        
        public static IEnumerable<int> mapPositiveNumbers(int number)
        {
            IList<int> PositiveNumbers = new List<int>();
            if (number > 0)
                PositiveNumbers.Add(number);
            return PositiveNumbers;
        }
        
        public static int groupNumbers(int number)
        {
            return number / 10;
        }

        public static IEnumerable<KeyValuePair<int, int>> reduceNumbers(IGrouping<int, int> grouping)
        {
            return new[]
            {
               new KeyValuePair<int, int>(grouping.Key,grouping.Count())
            };
        }

        실행 결과는 다음과 같습니다.

         위의 예를 통해 이 매핑 및 축소는 이해하기 훨씬 쉽습니다. 이는 실제로 비즈니스 템플릿을 작성하는 특정 방법(필터 → 그룹 → 병합)입니다. 병렬 프로그래밍에서는 이와 같은 작성 방법을 동일한 템플릿 코드를 통해 구현할 수 있습니다.

2. 집계

        집계는 병렬 애플리케이션에 사용되는 또 다른 일반적인 디자인 패턴입니다. 병렬 프로그램에서 데이터는 여러 스레드를 통해 코어 전체에서 처리될 수 있도록 단위로 구분됩니다. 어떤 시점에서는 모든 관련 소스 데이터를 사용자에게 표시하기 전에 결합해야 합니다.

        이 책의 예제에서는 PLINQ 코드를 사용하는 예제만 설명하며 그에 따라 코드도 작성하겠습니다.

        private void RunAggregation()
        {
            var L = Utils.GetOrderList(10);

            var L2 = L.AsParallel()
                   .Select(TestFunction.IntToString)//并行处理
                   .ToList();//合并

            foreach (var item in L2)
                Debug.Log(item);
        }
        
        public static string IntToString(int x)
        {
            return $"ToString_{x}";
        }

        위 코드를 실행한 결과는 다음과 같습니다.

         보시다시피 이 작동 모드는 순서를 보장합니다(소스 데이터는 목록임).

        일반적으로 잠금 및 동기화와 같은 추가 처리를 피하기 위해 PLINQ와 같은 구문을 사용하거나 동시 컬렉션을 사용합니다. 이렇게 하면 잠금, 동기화 등을 수동으로 처리할 필요성이 줄어듭니다.

3. 포크/병합 모드

        Fork/Join 패턴에서는 작업이 비동기식으로 실행될 수 있는 일련의 작업으로 분기(분할)된 다음 병렬화의 요구 사항 및 범위에 따라 동일한(또는 다른) 순서로 분기가 병합됩니다.

        분기/병합 패턴의 몇 가지 일반적인 구현은 다음과 같습니다.

  • 평행.용

  • 병렬.ForEach

  • 병렬.호출

  • 시스템.스레딩.카운트다운이벤트

        이러한 동기화 프레임워크를 사용하는 개발자는 동기화 오버헤드에 대한 걱정 없이 신속하게 개발을 구현할 수 있습니다(시스템은 이미 내부적으로 동기화를 처리하며, 실제로 추가 오버헤드가 허용되지 않는 경우 이러한 API를 사용하여 최적화할 방법이 없습니다).

        분기/병합 모드를 사용하여 이전 코드를 수정해 보겠습니다.

        private void RunForkJoin()
        {
            var L = Utils.GetOrderList(10);

            ConcurrentQueue<string> queue = new ConcurrentQueue<string>();

            Parallel.For(0, L.Count, x =>
            {
                var ret = IntToString(x);
                queue.Enqueue(ret);
            });

            while (queue.Count > 0)
            {
                string str;
                if (queue.TryDequeue(out str))
                    Debug.Log(str);
            }
        }

        이번에는 실행 결과를 살펴보겠습니다.

         분명히 순서가 어긋나 있으며 이 모드는 원래 데이터 순서에 따라 데이터를 처리하지 않습니다. 이것도 이 모드의 특징 중 하나인데, 순서대로 병합할지 여부를 선택할 수 있습니다.

4. 추측 처리 모드

        투기적 처리 패턴은 대기 시간을 줄이기 위해 높은 처리량에 의존하는 또 다른 병렬 프로그래밍 패턴입니다.

        추론적 처리 패턴:

        동시에 여러 처리 작업이 있는데 어떤 방법이 가장 빠른지 모르는 경우. 따라서 실행된 첫 번째 완료된 작업이 출력되고 다른 작업의 처리 결과는 무시됩니다.

        다음은 추측적 처리 패턴을 작성하는 일반적인 방법입니다.

        //选择一个最快执行方法的结果并返回
        public static TResut SpeculativeForEach<TSource, TResut>(TSource source, IEnumerable<Func<TSource, TResut>> funcs)
        {
            TResut result = default;

            Parallel.ForEach(funcs, (func, loopState) =>
            {
                result = func(source);
                loopState.Stop();
            });

            return result;
        }

        //返回特定方法的最快执行结果并返回
        public static TResut SpeculativeForEach<TSource, TResut>(IEnumerable<TSource> source, Func<TSource, TResut> func)
        {
            TResut result = default;

            Parallel.ForEach(source, (item, loopState) =>
            {
                result = func(item);
                loopState.Stop();
            });

            return result;
        }

        이 방법으로 작성하면 하나의 결과만 반환되며, 먼저 완료된 작업이 반환됩니다. 그러나 다른 작업은 계속 실행될 수 있지만 결과는 반환되지 않습니다.

        예를 들어 여기서는 방법 1을 선택합니다. 호출 코드는 다음과 같습니다.

        private void RunSpeculativeMethod_1()
        {
            Debug.Log($">===== RunSpeculativeMethod_1 开始 =====<");
            var L1 = new List<Func<int, string>>
            {
                IntToString,
                IntToString2
            };

            string result = SpeculativeForEach(4, L1);
            Debug.Log($"运行结果:{result}");
        }

        2번 연속으로 실행한 결과는 다음과 같습니다.

         첫 번째는 IntToString2의 결과를 사용하고 두 번째는 IntToString의 결과를 사용합니다.

5. 지연 모드

        즉, 사용될 때만 생성되는데, 이는 지연 로딩이다. 이에 대해서는 이전 장에서 자세히 소개했으며 여기서는 반복하지 않겠습니다.

        자세한 내용: 지연 초기화를 사용하여 성능 향상

[C#] 병렬 프로그래밍 실습: 지연된 초기화를 사용하여 성능 향상_Magician Dix의 블로그 - CSDN 블로그는 이전 장에서 코드 성능을 향상하고 동기화 오버헤드를 줄이는 데 도움이 될 수 있는 C#의 스레드로부터 안전한 동시 컬렉션에 대해 논의했습니다. 이 장에서는 사용자 정의 구현과 함께 내장 구문을 사용하는 것을 포함하여 성능 향상에 도움이 될 수 있는 추가 개념에 대해 설명합니다. 이 장의 주요 내용은 지연된 초기화를 통해 성능을 향상시키는 것인데 비교적 간단합니다. https://blog.csdn.net/cyf649669121/article/details/131780600

6. 공유 상태 모드

        주로 [C#] 병렬 프로그래밍 실습: 동기화 프리미티브(1)_Magician Dix의 블로그-CSDN 블로그에서 공유 상태 패턴(실제로는 매우 복잡해 보이는 다양한 잠금) 고급 구현을 소개했습니다.

        하지만 너무 많이 잠글 수는 없으며 그렇지 않으면 성능이 저하될 수 있으므로 잠금 없는 코드도 최대한 구현해야 합니다.


7. 이 장의 요약

        이 장에서는 실제로 다양한 템플릿의 예인 다양한 병렬 프로그래밍 패턴을 소개합니다. 물론 여기에 언급된 내용이 모든 것을 포괄할 수는 없으며 단지 참고 사항일 뿐입니다. 이 시점에서 멀티스레드 프로그래밍에 대한 연구가 끝났고, 책의 내용도 끝났습니다. 앞으로 추가되는 내용이 있으면 이 시리즈에 추가하겠습니다.

        멀티스레딩을 실행하려면 프로젝트에서 여전히 더 많은 연습이 필요합니다.

        이 튜토리얼로 엔지니어링 배우기: Magician Dix/HandsOnParallelProgramming · GitCode

추천

출처blog.csdn.net/cyf649669121/article/details/131954214