C# では並列化の実装が簡単に出来るようにと、便利な下記のステートメント(クラス)が用意されています。
  • Parallel.For()
  • Parallel.ForEach()
  • Parallel.Invoke()
なるほどぉと for や foreach で記述されている箇所を単純に置き換えると、割と高確率で例外で落ちます。これは、並列動作をしている各ブロックから、同じオブジェクトを参照することによる衝突が原因であることが多いです。
例えば、今私が作成しているマップエディタ内にこのような処理がありました。
※ 説明用にコード自体は端折っています(キャストも省略)。

    foreach (var rsc in layers)
    {
        rsc.Build();
    }
これを単純に置き換えると下記のような実装になります。

    Parallel.ForEach(layers, rsc =>
    {
        rsc.Build();
    });
実行させると、正常に動作することもありますが、例外で落ちることもあります。
これは rsc にはいくつかの種類があり、その同じ種類のクラスオブジェクトが、別に管理されている同一のオブジェクトを参照しようとしているために衝突したためです。この例だと、rsc(リソース)の Build(構築)には、元となる画像を参照しているため、同時に参照しようとしているためとなります。

衝突を回避する方法はいくつか考えられます。例えば…
  • リソースクラス毎に参照先画像を別々に保持する
  • 同じ種類のリソースは同時に並列に走らないようにする
前者の対応は、全て並列で実行できる代わりに、メモリ的に不利なのと同じモノを複数箇所に存在することでエンバグの可能性が出てきます。実行前にコピーで受け渡したとしても、若干ですが前処理時間がかかります。

後者の対応は、同じ種類毎に振り分ける前処理が必要となります。メモリ的には有利ですが、同じ種類のリソースが多数存在すると、結局並列では走らないため実行速度は向上しなくなります。

今回は現時点で処理速度は既にそこそこ満足いく状態にありましたので、後者の実装を選択しました。実装は下記のようになります。

    // リソースタイプ毎に振り分ける
    var rscBGs = layers.Where(i => i.Type == RscType.Ground);
    var rscObjs = layers.Where(i => i.Type == RscType.Object);
    var rscAtrbs = layers.Where(i => i.Type == RscType.Attribute);

    // 再構築する
    Parallel.Invoke(
        () => { foreach (var rsc in rscBGs) rsc.Build(); },
        () => { foreach (var rsc in rscObjs) rsc.Build(); },
        () => { foreach (var rsc in rscAtrbs) rsc.Build(); }
    );
これで無事、並列処理で例外が起きることはなくなりました。