C#非同期・並列処理いろいろ

(An [English version of this article](https://www.linkedin.com/pulse/topics-asynchronous-parallel-processes-c-hideki-saito/) is also available.)

C#ではいくつかの並列処理の方法が用意されていますが、それらに関してのメモです。C#が備える非同期・並列処理を網羅的にカバーしているわけではありませんが、個人的に使用しているものを中心に書いています。

# タスク系
## async/await処理

特に入出力関連で、<code>async</code>処理は広く行われていますので、C#をそれなりに利用している人は必ずといって見たことがある処理だと思います。

構文的には以下のようになります。

```csharp
private async Task Caller()
{
  await DoProcess();
}

private async Task DoProcess()
{
  // 処理
}
```

上記の場合は普通に<code>await</code>をかけているだけなので、事実上、同期処理と変わりません。これ役に立つのはプロセスを非同期的に行う必要があり、呼び出し側でその他の処理を行う余地がある場合です。例えば、以下のようなプロセスの場合、<code>await process</code>の部分で、処理が終了するまで待機します。

```csharp
private async Task Caller()
{
  var process = DoProcess();
  // 他のことを継続して実行
  await process;
}

private async Task DoProcess()
{
  // 非同期処理
}
```

<code>async</code>を使う場合は、戻り値は<code>Task</code>を使用します。<code>Task</code>を使用する場合、戻り値を指定することはできませんので、戻り値を返したい場合は型を指定し、次のようにします。

```csharp
private async Task Caller()
{
  var process = DoProcess();
  // 他のことを継続して実行
  await process;
}

private async Task<int> DoProcess()
{
  int x;
  // 非同期処理
  return x;
}
```

尚、<code>await</code>を使うのには呼び出し側が<code>async</code>で定義されている必要がありますが、そうでない場合は、以下のような構文が利用できます。

```csharp
private Task Caller()
{
  DoProcess().Wait();
}

private async Task DoProcess()
{
  // 非同期処理
}
```

非同期メソッドが戻り値を持つ場合は、<code>.Wait()</code>の代わりに<code>.Result</code>を使います。

```csharp
private Task Caller()
{
  var result = DoProcess().Result;
}

private async Task<int> DoProcess()
{
  int x;
  // 非同期処理
  return x;
}
```

<code>async</code>で定義されたメソッドにおいて、<code>await</code>が使われていないと警告が出ます。これは以下で抑制できます。

```csharp
private async Task Caller()
{
  await Task.CompletedTask;
}
```

この形式は、非同期的な処理（入出力等）完了する時間が特定できない場合の処理などにおいて役に立ちますが、並列処理としての使用は非常に冗長的になり、あまり快適にはなりません。そういった場合は他の方法を使用します。

## BackgroundWorker

もう一つ非同期の処理を実現する方法として、BackgroundWorkerがあります。これはWindows Formで非同期処理を実現したい場合において、使用できる（GUIの更新を伴う<code>async</code>や<code>await</code>の使用は安全ではないため）ので代わりに使用できる他、進捗ステータスのレポート、終了処理、キャンセル処理など定義することも可能で機能は多いのですが、その分、冗長な記述が必要になります。

処理中のライフタイムの管理が細かくできる点から、あるまとまった処理があり、それを一つの処理として非同期処理に回したい、という場合において便利に使用できる機能です。

# 並列系

## PLINQ（コレクションに対しての並列実行）

LINQは言わずもがな、C#が備える強力な機能ですが、これの並列処理版が用意されています。これをPLINQといいます。

PLINQにおいては<code>.AsParallel</code>を入れることにより、並列的なデータ取り出し等を行うことができます。

```csharp
var data = source.AsParallel().Where(n => n % 10 == 0).Select(n => n);
```

この場合においてはライブラリ側でコレクションが精査され、並列処理が安全、またはメリットがあるという結果になった場合において、並列処理になります。そうでなければ逐次的に実行されます。

どの程度の並行処理を行うか、という設定は<code>.WithDegreeOfParallelism()</code>で調整します。

```csharp
var data = source.AsParallel().WithDegreeOfParallelism(2).Where(n => n % 10 == 0).Select(n => n);
```

ネットワークリソースアクセスが絡む場合など、同時アクセスに制約がある場合に使用できます。これが指定されない場合、コア数を元に自動的に設定されます。

この状態においては順序に関しては保証されないので、順序が重要になってくる場合は<code>.AsOrdered()</code>を使用します。

```csharp
var data = source.AsParallel().AsOrdered.Where(n => n % 10 == 0).Select(n => n);
```

これは使用しない場合に比べ、オーバーヘッドがより高くなります。

データの取り出しの他、何らかの処理をしたい、という場合があるかと思いますが、その場合は.ForAll()を使うことができます。

これを使用すると、コレクションに対し、ラムダ式を指定し、非同期的に処理を行うことができます。

```csharp
source.AsParallel().ForAll(p => {
  p.value = p.value * 2;
});
```

この実行時に<code>.AsOrdered()</code>も使用することができますが、並列処理が開始される順序は既知である場合においても処理が実際に終了する時間は開きが出てくるため、必ずしも順序が保全されて出てくるとは限りませんので、この場合は後ほどソートなどを行う必要が出てくると思います。

## Parallel処理（Task Parallel Library)

PLINQはコレクションに対して行うことのできる処理ですが、汎用的に並列処理を使用するための機構として、Task Parallel Libraryというものが用意されています。

これは以下のような構文を持ちます。

```csharp
Parallel.ForEach(source, p => {
  p.value = p.value * 2
});
```

PLINQと似ていますが、これは実際にはPLINQが同じ機構を利用しているためです。PLINQでは用意されていない機構として、<code>Parallel.For</code>も使用できます。

```csharp
Parallel.For(0, 100, index => {
 // 処理
});
```

これはForループに似た機構を並列的に実行することができます。

## ロック

並列的に実行しながら、他のコレクションに結果を追加していきたい、というようなニーズがあるかと思います。例えば次のようなシーンです。


```csharp
source.AsParallel().ForAll(p => {
  p.value = p.value * 2;
  result.Add(p);
});
```

これはこのままだと問題が発生します。というのも、<code>result.Add(p)</code>の実行タイミングによっては正常に追加されないからです。

このような場合は、データをロックする必要があります。

```csharp
source.AsParallel().ForAll(p => {
  p.value = p.value * 2;
  lock(result)
  {
    result.Add(p);
  }
});
```

# その他

C#ではその他、よりローレベルなスレッド処理を実現するための機構が用意されています。より複雑な処理などを行うために必要な処理を実装するためにはこれらを使用することになります。

# 資料
マイクロソフトの資料で非同期・並列処理に関しては非常に詳しくまとめられています。

- [.NET での並列処理、コンカレンシー、および非同期プログラミング](https://docs.microsoft.com/ja-jp/dotnet/standard/parallel-processing-and-concurrency)

また、上記の資料にはここでは取り上げていない、それぞれの機能の更に詳細なメソッドやパラメーターなども説明されています。

文中で紹介した、BackgroundWorkerについては以下を参照して下さい。

- [BackgroundWorker クラス](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.backgroundworker)

# 最後に

多彩なデータを取り扱う場合に置いて非同期処理や並列処理は重要になります。非同期、並列処理は複雑になりがちですが、C#ではそれを簡単に実装するための機構が用意されています。解決する課題によって使用可能なツールが多数用意されているので、この記事がその活用の入り口になれればと考えております。