この章では、並列コードの問題シナリオを理解し、並列プログラミング/非同期手法を使用して問題を解決することに焦点を当てて、並列プログラミング パターンを 紹介します。この章では、最も重要なプログラミング パターンをいくつか紹介します。
このチュートリアルでエンジニアリングを学びましょう: Magician Dix/HandsOnParallelProgramming · GitCode
1.MapReduceモード
MapReduce は、サーバー間での大規模なコンピューティング要件など、ビッグ データの処理の問題を解決するために導入されました。このモードはシングルコア コンピューターで使用できます。
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 を 1 つのグループ、10 ~ 19 を 1 つのグループ、など)。
-
各グループの数を数えます。
次に、上記の 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. 集計
集約は、並列アプリケーションで使用されるもう 1 つの一般的な設計パターンです。並列プログラムでは、データは複数のスレッドを介してコア間で処理できるようにユニットに分割されます。ある時点で、ユーザーに表示する前に、関連するすべてのソース データを結合する必要があります。
この本の例では、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. フォーク/マージモード
フォーク/結合パターンでは、作業は非同期で実行できる一連のタスクにフォーク (分割) され、並列化の要件と範囲に応じて同じ (または異なる) 順序でフォークがマージされます。
フォーク/マージ パターンの一般的な実装の一部は次のとおりです。
-
パラレル用
-
Parallel.ForEach
-
Parallel.Invoke
-
System.Threading.CountdownEvent
これらの同期フレームワークを使用する開発者は、同期のオーバーヘッドを気にすることなく、開発を迅速に実装できます (システムはすでに同期を内部で処理しています。実際、追加のオーバーヘッドが許容できない場合、これらの 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. 投機的処理モード
投機的処理パターンは、待機時間を短縮するために高スループットに依存するもう 1 つの並列プログラミング パターンです。
投機的処理パターン:
同時に複数の処理タスクがあるが、どの方法が最も速いかわからない場合。したがって、最初に実行された完了したタスクが出力され、他のタスクの処理結果は無視されます。
以下は、投機的処理パターンを記述する一般的な方法です。
//选择一个最快执行方法的结果并返回
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 つだけ返され、最初に完了したタスクが返されます。ただし、他のタスクは引き続き実行できますが、結果は返されません。
ここでは例としてメソッド 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回連続で実行すると、結果は次のようになります。
1 回目は IntToString2 の結果を使用し、2 回目は IntToString の結果を使用します。
5. ディレイモード
つまり、使用されたときにのみ作成され、遅延読み込みになります。これについては前の章で詳しく紹介したので、ここでは繰り返しません。
詳細: 遅延初期化を使用してパフォーマンスを向上させる
6. 共有状態モード
これは主に[C#] 並列プログラミングの実践: 同期プリミティブ (1)_Magician Dix のブログ - CSDN ブログで共有ステート パターン (実際にはさまざまなロック、非常に複雑に見える) の実装が紹介されています。
ただし、ロックしすぎるとパフォーマンスが低下するため、できる限りロックのないコードを実装する必要があります。
7. この章の概要
この章では、さまざまな並列プログラミング パターンを紹介します。これらは実際にはさまざまなテンプレートの例です。もちろん、ここで述べた内容がすべてを網羅するものではありませんが、参考程度にとどめてください。この時点で、マルチスレッド プログラミングの学習は終了し、本の内容は終了しました。今後追加がある場合は、このシリーズに追加されます。
マルチスレッドを実践するには、プロジェクトでさらに練習する必要があります。
このチュートリアルでエンジニアリングを学びましょう: Magician Dix/HandsOnParallelProgramming · GitCode