C#でListの内の要素を削除したときに時々例外が発生してアプリが落ちる件の続きです。

非同期処理をしているときにforeach文を実行するときは注意が必要

foreach文のなかでリストの要素の追加と削除をすることはできません。「System.InvalidOperationException: コレクションが変更されました。列挙操作は実行されない可能性があります」という例外が発生します。

気をつけないといけないのは非同期処理をしているときです。リストの要素の加除とforeach文が別スレッドでおこなわれる場合は例外が発生する危険性があります。

以下のコードはコンストラクタ内で非同期処理でループを回してMyClassオブジェクトのNumberプロパティの総和を計算しています。ボタンをクリックすると新しいオブジェクトをリストに追加させようとしているのですが、例外が発生してしまいます。これはforeach文が実行されているときにリストの要素が変更されたためです。foreach文で重い処理を実行している場合、リストに新しい要素を追加するタイミングとかぶるため、ほぼ確実に例外が発生します。一方でThread.Sleep(10);がないと例外が発生する可能性はほとんどありません(非同期処理をしている以上、100%安全というわけではない)。

43行目で例外発生とありますが、それが上記コード内の①の部分です。

リストをコピーしてからforeach文を実行すれば例外は回避できる?

リストをコピーしてコピーしたものでforeach文を実行すれば、foreach文が実行されているときにmyClassesの要素が変更されても問題はおきません。

ただし、非同期処理 foreach文での例外発生を回避するのであればリストのコピーをつくればよいとは断言できません。リストのコピーを作成している最中に要素の加除がおこなわれたらどうなるでしょうか?これが非同期処理の怖いところです。

以下のコードはコンストラクタ内でリストのなかに10個の要素を格納して、以降は要素数が偶数であれば新しい要素を先頭に追加、奇数であれば先頭の要素を削除するという処理を無限ループで繰り返しています。常にリストの要素は変化しているわけです。このような状態でリストをコピーしてforeach文を実行してみます。

問題なく処理ができる場合もあるのですが、高確率で例外が発生します。そして例外が発生するのはlist.ToList();の部分です。

どうもlist.ToList()を実行中にリストの要素の加除がおこなわれたのが原因のようです。

排他ロックで解決

ではどうすればよいのか?

マルチスレッドプログラムにおいて、 単一のリソースに対して、 複数の処理が同時期に実行されると、 破綻をきたす部分をクリティカルセクションを呼びます。クリティカルセクションでは排他制御をおこなうなどして処理途中の状態にあるときには他からアクセスできないようにしなければなりません。

そのひとつの方法がlock文です。上記で実験したlist.ToList()は不要です。lockされているときにおこなわれようとしたもう一つの処理は前の処理が完了してからおこなわれます。