トップ回答者
Dictionary`2.Insert の再現手順

質問
-
マルチスレッドの処理系アプリケーションで、現地にて次の例外が発生しました。
---------------------------------------------------------------------------------
インデックスが配列の境界外です。
場所 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
場所 hogehoge.hogehoge()
---------------------------------------------------------------------------------複数のスレッドから同時に Dictionary にアクセスされて発生したと想定し、Add するタイミングに別スレッドから Remove するテストプログラムを作成してみたところ、Insert で例外が発生したのですが、StackTrace が Add -> Insert -> Resize と出力されました。
---------------------------------------------------------------------------------
インデックスが配列の境界外です。
場所 System.Collections.Generic.Dictionary`2.Resize()
場所 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
場所 System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
場所 hogehoge.hogehoge()
---------------------------------------------------------------------------------対処としてはスレッドセーフになるよう Dictionary を ConcurrentDictionary に変更しようと思っているのですが、出力される StackTrace が少し異なるため、この対処でよいか判断がつきません。
Add 以外で Insert が Call されることはあるのでしょうか?Dictionary クラスの Insert は直接 Call 出来ないと思うのですが、似たようなご経験がある方がいらっしゃいましたら、どのようなやり方をすれば、現地と同様な例外を発生させられるかアドバイスいただけると助かります。
宜しくお願い致します。
【環境】
Windows 2008 Server R2
.NET Framework 4.0
【補足】
・運用後、1ヶ月ほど連続稼動しています。
・試験環境での再現は出来ていません。
・現地アプリケーション再起動後、再現しなくなりました。
回答
-
スタックトレースの何を問題としているのでしょうか。
マルチスレッド動作ですから、.NET Framework内のどの処理とどの処理が同時に行われたかでエラー個所は簡単に変わります。Insert()でエラーが発生するかResize()で発生するかはその違いです。
もう1点、実行時最適化でAdd()メソッドがインライン展開されてスタックに残らない可能性もあります…可能性なのでわかりませんが。
気になるなら.NET Frameworkのソースコードを参照してみてはどうでしょうか。
それと稼働中のシステムだからこそ、データ型は変更すべきではないように思いますが…。 -
まず、マルチスレッドによる障害を再現しようとするのは無茶です。
ある程度予想はできますが、現象を再現させて云々というのは一般に無理があります。Dictionaryの場合、ハッシュテーブルの再作成などで内部配列の解放と再挿入が行われる可能性があると思われますので、IndexOutOfRangeExceptionが発生するとすればその辺りのタイミングが怪しいとは推測できますが。
公開されているソースを追えばもっと細かく調査できるでしょうが、それでも推測の域は出ませんし、再現させるのは難しいです。
元々スレッドセーフではないのですから、何が起こってもおかしくないというところで納得しておく方が健全です。
※自分の興味で調べてみるのは勉強にもなりますし別にいいんですけどね。あと、ConcurrentDictionaryにすると確かに単一操作はスレッドセーフにはなりますが、置き換えただけでマルチスレッドによる問題を完全に回避できるわけではない点に注意してください。
例えば、キーをチェックした後にアクセスするなどというコードは、一般にマルチスレッドでは問題が発生しやすいです。
※気になるならCheck-Then-Act とかのキーワードで調べてみてください。パフォーマンスについては、ConcurrentDictionaryにしたところでほとんど変わらないと思いますよ、おそらく。
すべての返信
-
-
まずDictionaryクラスはスレッドセーフではありません。これが意味することは、
- Remove()によって項目数が減少し、内部に保持している配列を縮小しようとしている最中に
- Insert()によって、内部に保持している配列の余剰分を使おう
という処理が同時に走った場合、余剰部分がRemove()によって解放されてしまい今回のようなインデックスの範囲外例外が発生する可能性は十分にあります。
Add()以外にもdictionary[key] = value;でAdd()できます。(正確にはupdate or insertの動作をする)。
ConcurrentDictionaryを使わなくても、
lock( dictionary ) dictionary.Add( key, value );
のように、dictionaryにアクセスする際にlockすればそれまでですが。 -
System.Threading.Monitor.Enter(Dictionaryインスタンス); try { --Dictionaryインスタンス.Add(・・・); } finally { System.Threading.Monitor.Exit(Dictionaryインスタンス); }
こういう排他ロックのやり方があるんですか!しかもタイムアウト時間が指定できるのはすごい!知りませんでした。勉強になります。いつか使わせていただきます。
ただ、今回に限っては既に運用中のシステムで、全てのDictionary操作箇所に排他制御をかけるのはインパクトや不具合のもととなることを考えて、パフォーマンスは多少落ちますが、ConcurrentDictionary を使って排他ロックをかけようと思っています。
例えば・・・
Dictionary<int, int> dictionary = new Dictionary<int, int>();
↓
IDictionary<int, int> dictionary = new ConcurrentDictionary<int, int>();
のような感じで。#ただ、このような使い方をしている方は検索してもいらっしゃらなかったので、
#使い方としてだめなのかもしれませんが、一応、テストプログラム上では排他
#がかけられているように見えました。あと、私が一番知りたいところは、↓このような例外を出すにはどのような手順を踏めばよいかというところです。
---------------------------------------------------------------------------------
インデックスが配列の境界外です。
場所 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
場所 hogehoge.hogehoge()
---------------------------------------------------------------------------------
何か心当たりがありましたら、些細な情報でもよいですので、ご教示いただけると助かります。 -
私の言葉足らずな質問に対し、わかりやすくご説明いただきありがとうございます。
dictionary[key] = value;も一応検証はしています。Addと似たような感じのStackTraceになったので、端折って書きませんでしたが、↓このように出力されました。
---------------------------------------------------------------------------------
インデックスが配列の境界外です。
場所 System.Collections.Generic.Dictionary`2.Resize()
場所 System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
場所 System.Collections.Generic.Dictionary`2.set_Item(TKey key, TValue value)
場所 hogehoge.hogehoge()
---------------------------------------------------------------------------------やはり、現地で出力されたものとは少し異なりました。。。
対処につてはインパクトを考えて ConcurrentDictionary を使おうと思っていますが、あまりに性能が悪くなった場合は、ご提案いただいている lock を使わせていただきたいと思います。ありがとうございますm(_ _)m
-
スタックトレースの何を問題としているのでしょうか。
マルチスレッド動作ですから、.NET Framework内のどの処理とどの処理が同時に行われたかでエラー個所は簡単に変わります。Insert()でエラーが発生するかResize()で発生するかはその違いです。
もう1点、実行時最適化でAdd()メソッドがインライン展開されてスタックに残らない可能性もあります…可能性なのでわかりませんが。
気になるなら.NET Frameworkのソースコードを参照してみてはどうでしょうか。
それと稼働中のシステムだからこそ、データ型は変更すべきではないように思いますが…。 -
まず、マルチスレッドによる障害を再現しようとするのは無茶です。
ある程度予想はできますが、現象を再現させて云々というのは一般に無理があります。Dictionaryの場合、ハッシュテーブルの再作成などで内部配列の解放と再挿入が行われる可能性があると思われますので、IndexOutOfRangeExceptionが発生するとすればその辺りのタイミングが怪しいとは推測できますが。
公開されているソースを追えばもっと細かく調査できるでしょうが、それでも推測の域は出ませんし、再現させるのは難しいです。
元々スレッドセーフではないのですから、何が起こってもおかしくないというところで納得しておく方が健全です。
※自分の興味で調べてみるのは勉強にもなりますし別にいいんですけどね。あと、ConcurrentDictionaryにすると確かに単一操作はスレッドセーフにはなりますが、置き換えただけでマルチスレッドによる問題を完全に回避できるわけではない点に注意してください。
例えば、キーをチェックした後にアクセスするなどというコードは、一般にマルチスレッドでは問題が発生しやすいです。
※気になるならCheck-Then-Act とかのキーワードで調べてみてください。パフォーマンスについては、ConcurrentDictionaryにしたところでほとんど変わらないと思いますよ、おそらく。