C#でマルチスレッドとIEnumerableを組み合わせる際の注意点についてまとめました。
はじめに
C#ではTaskなどのクラスを使うことでマルチスレッドで処理を行えます。
またIEnumerableを使うと処理を遅延評価できます。
共に便利な機能ですが、これらを組み合わせて使った際に使い方を間違えると、
実はマルチスレッドで処理が行われていなかったり、
まとめられるはずの評価処理がすべてのスレッドで行われたりします。
本記事ではこのあたりについてまとめます。
IEnuemrableをサブスレッドから返すことを考える
今以下のようにIEnumerable<string>
を返す処理SomeHeavyEnumerable()
をサブスレッドに逃がすことを考えます。
// ダメな例です private async void Example() { // 別スレッドに処理を逃がしたい(逃がせてない) var result = await Task.Run(SomeHeavyEnumerable); foreach (var str in result) { // 結果を使って何らかの処理をする } } private IEnumerable<string> SomeHeavyEnumerable() { yield return "Foo"; yield return "Bar"; }
上のコードは一見SomeHeavyEnumerable()がサブスレッドで実行されるようにも見えますが、
IEnumerableの遅延評価が元のスレッドで行われているため、実際にはすべての処理が元のスレッドで実行されます。
SomeHeavyEnumerable()
をサブスレッドに逃がすには以下のように一度Taskの中でIEnumerableを評価する必要があります。
以下の例では配列を返すように変更しています。
private async void Example() { // 書き換えた var result = await Task.Run(() => SomeHeavyEnumerable().ToArray()); foreach (var str in result) { // 結果を使って何らかの処理をする } } private IEnumerable<string> SomeHeavyEnumerable() { yield return "Foo"; yield return "Bar"; }
サブスレッドで行う処理にIEnumerableを渡すことを考える
次に以下のようにサブスレッドで行う処理にIEnumerable型の引数を渡すことを考えます。
// ダメな例です private async void Example() { var strings = Enumerable.Range(0, 3).Select(x => x.ToString()); Parallel.For(0, 3, _ => { // すべてのスレッドでIEnumerableが評価されてしまう SomeHeavyMethod(strings); }); } private void SomeHeavyMethod(IEnumerable<string> strings) { foreach (var str in strings) { // 何らかの処理 } }
上の例ではIEnumerableの評価が書くスレッド内で行われてしまいます。
目的によりますが、同じ処理を複数のスレッドで走らせるのは無駄です。
以下のようにIEnuemrableを事前に評価しておくことで、評価処理を元スレッドにまとめることができます。
private async void Example() { // ToArrayを付けて事前に評価 var strings = Enumerable.Range(0, 3).Select(x => x.ToString()).ToArray(); Parallel.For(0, 3, _ => { SomeHeavyMethod(strings); }); } private void SomeHeavyMethod(IEnumerable<string> strings) { foreach (var str in strings) { Debug.Log(str); } }