이 장에서는 병렬 프로그래밍 패턴을 소개하고 병렬 코드 문제 시나리오를 이해하고 병렬 프로그래밍/비동기 기술을 사용하여 문제를 해결하는 데 중점을 둡니다. 이 장에서는 가장 중요한 프로그래밍 패턴 중 일부를 소개합니다.
이 튜토리얼로 엔지니어링 배우기: 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);
}
이 기능을 이해하기 위해 요구사항을 사용합니다.
-
소스 데이터는 -100~100 범위의 난수 1000개입니다.
-
양수를 필터링합니다.
-
10자리 숫자에 따라 그룹화합니다(0에서 9까지의 그룹 하나, 10에서 19까지의 그룹 하나 등).
-
각 그룹의 수를 센다.
그런 다음 위의 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. 지연 모드
즉, 사용될 때만 생성되는데, 이는 지연 로딩이다. 이에 대해서는 이전 장에서 자세히 소개했으며 여기서는 반복하지 않겠습니다.
자세한 내용: 지연 초기화를 사용하여 성능 향상
6. 공유 상태 모드
주로 [C#] 병렬 프로그래밍 실습: 동기화 프리미티브(1)_Magician Dix의 블로그-CSDN 블로그에서 공유 상태 패턴(실제로는 매우 복잡해 보이는 다양한 잠금) 고급 구현을 소개했습니다.
하지만 너무 많이 잠글 수는 없으며 그렇지 않으면 성능이 저하될 수 있으므로 잠금 없는 코드도 최대한 구현해야 합니다.
7. 이 장의 요약
이 장에서는 실제로 다양한 템플릿의 예인 다양한 병렬 프로그래밍 패턴을 소개합니다. 물론 여기에 언급된 내용이 모든 것을 포괄할 수는 없으며 단지 참고 사항일 뿐입니다. 이 시점에서 멀티스레드 프로그래밍에 대한 연구가 끝났고, 책의 내용도 끝났습니다. 앞으로 추가되는 내용이 있으면 이 시리즈에 추가하겠습니다.
멀티스레딩을 실행하려면 프로젝트에서 여전히 더 많은 연습이 필요합니다.
이 튜토리얼로 엔지니어링 배우기: Magician Dix/HandsOnParallelProgramming · GitCode