none
複数のバックグラウンドスレッドに繰り返し処理を依頼するとメインスレッドがロックする RRS feed

  • 質問

  • フォームから複数のバックグラウンドスレッドにBeginInvokeで処理を依頼し、そのコールバックメソッドからメインスレッドにBeginInvokeで表示更新と再処理を依頼する、ということを繰り返すとなぜかメインスレッドがロックしてしまいます。
    バックグラウンドスレッド上からGUIコントロールを操作したり、Application.DoEventsを呼んだり、スレッドプールが枯渇したりはしていません。
    原因がわからず対策もできない状況です。皆様のお知恵をお借りできれば幸いです。

    環境
    Windows XP SP2
    Visual Studio 2005
    マルチコア環境 (Core2 Duo)


    以下は再現コードです。
    フォームにボタン1つとテキストボックス2つを配置して下さい。
    環境によってうまく再現しない場合は冒頭の定数を 0 以外で色々変えてみて下さい。
    WORKER_COUNT = 1 だと発生しない(しにくい?)ようです。
    試しにCallback(IAsyncResult)メソッド内やCompleteAndContinue()メソッド内をクリティカルセクションにしたりもしてみましたが効果はありませんでした。

    よろしくお願いします。

    public partial class Form1 : Form
    {
        private const int COMPLETE_WORK_TIME = 10;
        private const int REQUEST_WORK_TIME = 30;
        private const int WORKER_COUNT = 10;
    
        private delegate void CompleteAndContinueDelegate();
        private delegate void RequestDelegate();
    
        private int _completeCount = 0;
    
        public Form1()
        {
            InitializeComponent();
        }
    
        private void button1_Click(object sender, EventArgs e)
        {
            // WORKER_COUNT 分バックグラウンドスレッドに処理を依頼する
            for (int index = 0; index < WORKER_COUNT; index++)
            {
                this.InvokeRequest();
            }
        }
    
        private void InvokeRequest()
        {
            // バックグラウンドスレッドにコールバックありで処理を依頼する
            RequestDelegate requestDelegate = this.Request;
            AsyncCallback callback = this.Callback;
            requestDelegate.BeginInvoke(callback, null);
        }
    
        private void Request()
        {
            // バックグラウンドでのダミー処理
            Thread.Sleep(REQUEST_WORK_TIME);
        }
    
        private void Callback(IAsyncResult ar)
        {
            // 依頼した処理の完了通知
            // ここはまだバックグラウンドスレッドで実行されている
            // メインスレッドに表示更新と次の処理の開始を依頼する
            CompleteAndContinueDelegate completeAndContinueDelegate = this.CompleteAndContinue;
            this.BeginInvoke(completeAndContinueDelegate);
    
            AsyncResult asyncResult = (AsyncResult)ar;
            RequestDelegate requestDelegate = (RequestDelegate)asyncResult.AsyncDelegate;
            requestDelegate.EndInvoke(ar);
        }
    
        private void CompleteAndContinue()
        {
            // メインスレッドでのダミー処理
            Thread.Sleep(COMPLETE_WORK_TIME);
    
            // 表示更新
            this._completeCount++;
            this.textBox1.Text = this._completeCount.ToString();
    
            int availableWorkerThreads;
            int availableCompletionPortThreads;
            ThreadPool.GetAvailableThreads(out availableWorkerThreads, out availableCompletionPortThreads);
            int maxWorkerThreads;
            int maxCompletionPortThreads;
            ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads);
            this.textBox2.Text = string.Format("{0} / {1}", maxWorkerThreads - availableWorkerThreads, maxWorkerThreads);
    
            // 次の処理をバックグラウンドスレッドに依頼する
            this.InvokeRequest();
        }
    }
    
    • 編集済み akari 2009年9月25日 16:44 コードブロックの改行がおかしかったので修正
    2009年9月25日 16:41

すべての返信

  • フォームから複数のバックグラウンドスレッドにBeginInvokeで処理を依頼し、そのコールバックメソッドからメインスレッドにBeginInvokeで表示更新と再処理を依頼する、ということを繰り返すとなぜかメインスレッドがロックしてしまいます。
    UI が更新されなくなる、UI が応答しないことを「メインスレッドがロックする」と表現しているのでしょうか?
    そうであれば、メインスレッドで実行される CompleteAndContinue で Thread.Sleep しているのが問題でしょう。その行をコメントアウトして実行してみて下さい。

    メインスレッドで Sleep することで完了通知を捌くスピードが足りなくなり、メインスレッドはひたすら完了通知を処理するだけで精一杯の状態に陥っているのでしょう。
    一般的に、10ms とはいえ、メインスレッドで Sleep を実行することはお薦めできません。
    解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。
    2009年9月25日 23:30
    モデレータ
  • スリープしてるのは再現のためで、もともとはここでそれなりに負荷のかかる処理が必要ということなのでしょう、多分。
    Control.BeginInvokeで投入された処理はUI系の処理より先に処理される?ような気がするので、
    例えばCompleteAndContinue内で適宜Application.DoEvents()して、
    UI系のメッセージを処理させてしまう(ずっと処理できない状態を防ぐ)ようにすれば、
    固まらなくはなると思います。

    2009年9月26日 1:27
  • スリープしてるのは再現のためで、もともとはここでそれなりに負荷のかかる処理が必要ということなのでしょう、多分。
    そうだと仮定しても、その「それなりに負荷のかかる処理」がメインスレッドで実行されるべきなのかも気にはなります。
    (メインスレッドで重たい処理をしているのであれば、何のためにワーカースレッドか分からない)
    再現コードでは Application.DoEvents で回避はできますが、安易に回避せず、設計を見直すべきだと思います。

    ただ、実際にどのような処理をやっているかが明記されていないので、言い切れる段階ではないのかもしれませんが…。
    解決した場合は、参考になった返信に「回答としてマーク」のボタンを利用して、回答に設定しましょう(複数に設定できます)。
    2009年9月26日 3:23
    モデレータ
  • こちらで実行したところ、DoEventsも非同期処理の完了を処理するようで、
    CompleteAndContinueにDoEevntsを追加しても、スタックオーバーフローで死ぬようになりますね。
    CompleteAndContinueでのInvokeRequest呼び出しにタイマをかませるしかないと思います。
            Stack<System.Windows.Forms.Timer> free_timers = new Stack<System.Windows.Forms.Timer>();
            private void InvokeRequestLater()
            {
                System.Windows.Forms.Timer tm;
    
                //free_timersは、メインスレッドのみ操作なので、ロック要らない。
                if (free_timers.Count == 0)
                {
                    tm = new System.Windows.Forms.Timer();
                    tm.Interval = 1; //inerval自体はそんなに重要じゃない。タイマは優先順位低いから。
                    tm.Tick += new EventHandler(tm_Tick);
                }
                else
                {
                    tm = free_timers.Pop();
                }
                tm.Start();
            }
    
            void tm_Tick(object sender, EventArgs e)
            {
                System.Windows.Forms.Timer timer = (sender as System.Windows.Forms.Timer);
                this.InvokeRequest();
                timer.Stop();
                free_timers.Push(timer);
            }
    

    jzkey
    2009年9月26日 10:48
  • こちらで実行したところ、DoEventsも非同期処理の完了を処理するようで、
    CompleteAndContinueにDoEevntsを追加しても、スタックオーバーフローで死ぬようになりますね。
    ひょっとして最後に入れました?
    this.InvokeRequest();
    の前に入れないとだめだと思いますよ。
    ※投入する前にUI側を完了させるために
    2009年9月26日 12:58
  • みなさん、ご回答ありがとうございます。

    Azuleanさん、なちゃさんの返信を読んで、上記再現コードは実際の現象をうまく再現できていないことがわかりました。
    見た目上似たような挙動(UIが反応しなくなる)が出たため再現できたと勘違いしていました。申し訳ありません。
    そして、再現できたと思ったので明記しませんでしたが、実際に現象が起きているのはVS2005ではなくVS2003の環境です。
    今手元にVS2003環境がないのでとりあえずVS2005で再現しそうなコードを書いた次第です。

    実際にはもっと複雑なことをやっているので、ちゃんと現象が再現するようなミニマムコードが書けたらまた質問したいと思います。
    Control.BeginInvokeで投入された処理はUI系の処理より先に処理される?ような気がするので、 
    これは知りませんでした。CompleteAndContinue()にTrace.WriteLine(this._completeCount)を仕込んでみたらロックせず動作していることが確認できました。
    実際のコードでもこれが影響していないか考えてみます。
    ただ、実際にどのような処理をやっているかが明記されていないので、言い切れる段階ではないのかもしれませんが…。
    一応実際の処理について箇条書きですがもう少し詳しく説明しておきます。
    • 上記再現コードの内容は実際にはFormではなくUserControlで、WORKER_COUNTはフォーム上のUserControlのインスタンス数に相当します。
    • UserControlはOnPaintBackgroundで自身が保持するバッファーBitmapをe.Graphicsに転写します。
    • フォーム上のユーザー操作をトリガーに各UserControlに新しい描画データが渡り、表示更新が必要になります。
    • 表示更新が必要になったUserControlは自身が保持するワークBitmapに描画データをレンダリングするようバックグラウンドスレッドに依頼します。
    • バックグラウンドでのレンダリング中にまた新しい描画データが渡ってきたら、そのUserControlインスタンスが持つキューに溜めます。
    • レンダリングが終了したときにキューが溜まっていたらキューの末尾以外は捨てて、末尾の描画データをレンダリングするようバックグラウンドスレッドに依頼し直します。
      (上記再現コードとは違いCallback(IAsyncResult)メソッド内でthis.InvokeRequestします)
    • レンダリングが終了したときにキューが空ならバッファーBitmapにワークBitmapを転写し、さらにUserControlのいくつかのプロパティに依存した若干の加工をしてからthis.Invalidate()します。
      (CompleteAndContinueのThread.Sleepに相当。this.InvokeRequestはしません)
    • キューの操作/参照やその他で必要な排他は専用のロックオブジェクトを用意したりして適宜行っており、またlockの順序も揃えるなどクリティカルセクション起因でデッドロックしないように注意してあります。
    • フィールドは可能な限りreadonlyやvolatileで宣言してあります。
    従って、実際の処理では無限にループするわけではなく、ユーザーの操作が無くなればCallback(IAsyncResult)のInvokeRequestは実行されなくなりますから、一時的にUIの操作を受け付けなくなったとしても少し待てば復帰するはずです。
    ところが、ユーザーの操作時に非常にまれに表示が更新されずにアプリケーションがハングすることがあり、その際にはVisual Studioの一時停止ボタンを押すと、タスクマネージャーでアプリケーションを強制終了するまでVisual Studioまでもハングします。
    イベントログにはApplication Hang (イベントID 1002)が記録されます。
    あちこちにデバッグログを仕込んで試したところ、あるUserControlインスタンスでthis.ParentForm.BeginInvokeしたにも関わらず、CompleteAndContinueが実行されずにハング状態になっているようでした。
    ログを見る限りはクリティカルセクション起因ではデッドロックしていないようです。(メインスレッドがロックしたことによって最後の方のログが出力されていないだけかもしれませんが…)
    CompleteAndContinueでのInvokeRequest呼び出しにタイマをかませるしかないと思います。 

    すみません、上記の通り再現コード(再現できてないけど…)は簡略化されており、実際にはCompleteAndContinueにInvokeRequestはありません。

    うまくミニマムの再現コードが作れるかわかりませんが、もし上記の情報だけで「この辺を疑ってみたら?」というのがあれば再度ご指摘下さい。
    よろしくお願いします。

    • 編集済み akari 2009年9月26日 13:47 誤記修正(太字)
    2009年9月26日 13:32