none
Dictionary`2.Insert の再現手順 RRS feed

  • 質問

  • マルチスレッドの処理系アプリケーションで、現地にて次の例外が発生しました。

     

    ---------------------------------------------------------------------------------

    インデックスが配列の境界外です。
       場所 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ヶ月ほど連続稼動しています。

    ・試験環境での再現は出来ていません。

    ・現地アプリケーション再起動後、再現しなくなりました。

     

    2012年1月19日 1:34

回答

  • スタックトレースの何を問題としているのでしょうか。

    マルチスレッド動作ですから、.NET Framework内のどの処理とどの処理が同時に行われたかでエラー個所は簡単に変わります。Insert()でエラーが発生するかResize()で発生するかはその違いです。

    もう1点、実行時最適化でAdd()メソッドがインライン展開されてスタックに残らない可能性もあります…可能性なのでわかりませんが。

    気になるなら.NET Frameworkのソースコードを参照してみてはどうでしょうか。

    それと稼働中のシステムだからこそ、データ型は変更すべきではないように思いますが…。
    • 編集済み 佐祐理 2012年1月19日 7:12
    • 回答としてマーク kazki0729 2012年1月19日 10:38
    2012年1月19日 7:09
  • まず、マルチスレッドによる障害を再現しようとするのは無茶です。
    ある程度予想はできますが、現象を再現させて云々というのは一般に無理があります。

    Dictionaryの場合、ハッシュテーブルの再作成などで内部配列の解放と再挿入が行われる可能性があると思われますので、IndexOutOfRangeExceptionが発生するとすればその辺りのタイミングが怪しいとは推測できますが。
    公開されているソースを追えばもっと細かく調査できるでしょうが、それでも推測の域は出ませんし、再現させるのは難しいです。
    元々スレッドセーフではないのですから、何が起こってもおかしくないというところで納得しておく方が健全です。
    ※自分の興味で調べてみるのは勉強にもなりますし別にいいんですけどね。

    あと、ConcurrentDictionaryにすると確かに単一操作はスレッドセーフにはなりますが、置き換えただけでマルチスレッドによる問題を完全に回避できるわけではない点に注意してください。
    例えば、キーをチェックした後にアクセスするなどというコードは、一般にマルチスレッドでは問題が発生しやすいです。
    ※気になるならCheck-Then-Act とかのキーワードで調べてみてください。

    パフォーマンスについては、ConcurrentDictionaryにしたところでほとんど変わらないと思いますよ、おそらく。

    • 編集済み なちゃ 2012年1月19日 7:30
    • 回答としてマーク kazki0729 2012年1月19日 10:49
    2012年1月19日 7:24

すべての返信

  • Dictionary インスタンスへのアクセス時に排他ロックをかけてみてはいかがですか?

    ※Add , Clear , Remove 等の各処理を全て、下記の様に囲む事でロックできるかと・・・

    System.Threading.Monitor.Enter(Dictionaryインスタンス);
    try
    {
        --Dictionaryインスタンス.Add(・・・);
    }
    finally
    {
        System.Threading.Monitor.Exit(Dictionaryインスタンス);
    }

     

    • 編集済み aviator__ 2012年1月19日 2:18
    2012年1月19日 2:16
  • まずDictionaryクラスはスレッドセーフではありません。これが意味することは、

    • Remove()によって項目数が減少し、内部に保持している配列を縮小しようとしている最中に
    • Insert()によって、内部に保持している配列の余剰分を使おう

    という処理が同時に走った場合、余剰部分がRemove()によって解放されてしまい今回のようなインデックスの範囲外例外が発生する可能性は十分にあります。

    Add()以外にもdictionary[key] = value;でAdd()できます。(正確にはupdate or insertの動作をする)。

    ConcurrentDictionaryを使わなくても、

    lock( dictionary )
      dictionary.Add( key, value );
    
    

    のように、dictionaryにアクセスする際にlockすればそれまでですが。

    2012年1月19日 2:34
  • lockステートメントの方がシンプルですね。

    ※ちなみに lock ステートメントとMonitorクラスのEnter~Endは一緒です。

    (MonitorクラスだとTryLockでロック可能かどうかの検証及びタイムアウト時間の指定が出来ます。)

    2012年1月19日 2:51
  • 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()
    ---------------------------------------------------------------------------------
    何か心当たりがありましたら、些細な情報でもよいですので、ご教示いただけると助かります。

    2012年1月19日 6:25
  • 私の言葉足らずな質問に対し、わかりやすくご説明いただきありがとうございます。

    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

    2012年1月19日 6:37
  • スタックトレースの何を問題としているのでしょうか。

    マルチスレッド動作ですから、.NET Framework内のどの処理とどの処理が同時に行われたかでエラー個所は簡単に変わります。Insert()でエラーが発生するかResize()で発生するかはその違いです。

    もう1点、実行時最適化でAdd()メソッドがインライン展開されてスタックに残らない可能性もあります…可能性なのでわかりませんが。

    気になるなら.NET Frameworkのソースコードを参照してみてはどうでしょうか。

    それと稼働中のシステムだからこそ、データ型は変更すべきではないように思いますが…。
    • 編集済み 佐祐理 2012年1月19日 7:12
    • 回答としてマーク kazki0729 2012年1月19日 10:38
    2012年1月19日 7:09
  • まず、マルチスレッドによる障害を再現しようとするのは無茶です。
    ある程度予想はできますが、現象を再現させて云々というのは一般に無理があります。

    Dictionaryの場合、ハッシュテーブルの再作成などで内部配列の解放と再挿入が行われる可能性があると思われますので、IndexOutOfRangeExceptionが発生するとすればその辺りのタイミングが怪しいとは推測できますが。
    公開されているソースを追えばもっと細かく調査できるでしょうが、それでも推測の域は出ませんし、再現させるのは難しいです。
    元々スレッドセーフではないのですから、何が起こってもおかしくないというところで納得しておく方が健全です。
    ※自分の興味で調べてみるのは勉強にもなりますし別にいいんですけどね。

    あと、ConcurrentDictionaryにすると確かに単一操作はスレッドセーフにはなりますが、置き換えただけでマルチスレッドによる問題を完全に回避できるわけではない点に注意してください。
    例えば、キーをチェックした後にアクセスするなどというコードは、一般にマルチスレッドでは問題が発生しやすいです。
    ※気になるならCheck-Then-Act とかのキーワードで調べてみてください。

    パフォーマンスについては、ConcurrentDictionaryにしたところでほとんど変わらないと思いますよ、おそらく。

    • 編集済み なちゃ 2012年1月19日 7:30
    • 回答としてマーク kazki0729 2012年1月19日 10:49
    2012年1月19日 7:24
  • 大変、有用な情報、ありがとうございます!

    例外の箇所を特定したければ .NET Framework のソースコードを追って確認することも出来るんですね。
    JIT最適化でインライン展開されることも初めて知りました。

    データ型は変更すべきではないというご指摘もありがとうございます。
    踏まえてどのように対処するか検討いたしますm(_ _)m

    2012年1月19日 10:38
  • やはり、推測の域を超えることは難しいですか。ありがとうございます。

    ConcurrentDictionary も完全にスレッドセーフにはならないというご指摘は、大変参考になりました。 foreach や Linq で検索するときなども注意が必要そうですね。「Check-Then-Act 」について調べてみます。

    ConcurrentDictionary ですが、テストプログラムで測定してみましたが、Dictionary と比較すると全然違いました。少量のデータであればあまり気にすることは無いと思いますが。

    2012年1月19日 10:48
  • >ConcurrentDictionary も完全にスレッドセーフにはならないというご指摘

    一応、後から見た人が誤解しないように補足。

    ConcurrentDictinary クラス自体は保証された範囲でスレッドセーフな実装が行われています。

    ですが、ConcurrentDictionary クラスを使っユーザコードの記述によっては、結果としてマルチスレッド実行で問題を生じる可能性がある、ということです。

     

    2012年1月20日 4:08
    モデレータ