DOBON.NET DOBON.NETプログラミング掲示板過去ログ

UIスレッドのデットロック

環境/言語:[ C# .NET Framework4.0]
分類:[.NET]

いつもお世話になっております。

マルチスレッドでのUI操作における
UIスレッドのデットロックについて
ご鞭撻いただきたく書き込みさせていただきました。

非同期操作、かつ並列処理で、UI上のテキストボックスにテキストを追加する検証を行っています。
特定の非同期操作でUIスレッドのデッドロック?が発生しているように思えるのですが
デッドロックとなる原因・理由が理解できず、悩んでおります。
非同期のスレッド操作について、ご指導いただければ幸いです。

下部にソースコードを記載していますが
理解できていない箇所の要点です。

・BeginInvoke・Invokeを使ってTextBoxの内容を変更しようとするメソッドを
パラレルで呼び出すとフリーズする。(時がある)

フリーズのタイミング
1.Invoke呼び出し ⇒ほとんどフリーズする。プールスレッドのスレッド不足?
2.BeginInvoke呼び出し TextBoxの描画中にUIスレッド上からマウスでTextBoxをクリックするとフリーズする(これは多分デッドロック)

1については、並列処理数をかなり減らしてもフリーズするため、スレッドプールの不足とは考えにくく、
どういったスレッドの処理によって、フリーズしてしまうのかすら検討がつけれていません。

2については、UIからの操作と、非同期操作間で何らかのデッドロックが発生していると思うのですが、
同期操作にすると1と同じ現象になってしまうので、解消方法が見つけれていません。

対策として、スレッドを別に立ち上げて(new Thread)BeginInvoke操作を実行すると問題なく作動するのですが・・・
果たして、正しいお作法なのか・・・
(2でデッドロックするのに、別スレッドからの操作をして同期をとらなくても大丈夫なのかな・・・等)

別スレッドで操作する際のUIに対する処理のベストプラクティス等あれば教えていただきたいです。
(バックグラウンドワーカーの内部処理でイベントを使っているようですが、これが正なのでしょうか?)
恐れ入りますが、よろしくお願い致します。

開発言語 C# 4.0
環境 VisualStudio 2010

プロジェクト:
Windows From
 ├ textBox1(テキストボックス)
 ├ 各種処理実行するためのボタン

以下、処理の説明。
1.マニュアルスレッドを使った非同期処理かつバックグラウンド処理(New Thread)。
 //これは動く。
2.プールスレッドを使った非同期処理(BeginInvoke)
 //実行中にTextBoxを触るとフリーズ。
3.プールスレッドを使った同期処理(Invoke)
 //フリーズする。

 ※123いずれも並列処理を行い、途中に重たい処理を想定してThread.Sleepしている。
  内部で以下のメソッドを呼び出して、TextBoxを更新している。
4.textBox1.BeginInvokeを使ったTextを変更するメソッド。EndInvokeを呼ばず、同期は待たない(12で利用)
5.textBox1.Invokeを使ったTextを変更するメソッド(3で利用)

-----実装コード-----
/// マニュアルスレッドにより実行
/// 一番安定している。(お作法として正しいか?)
private void button1_Click( object sender, EventArgs e )
{
//マニュアルスレッド・バックグラウンドでパラレル処理実行
var t = new Thread( () => {
Parallel.For( 0, 20, id => {
AddMsgUseBeginInvoke( id + "開始" );
System.Threading.Thread.Sleep( 300 );
AddMsgUseBeginInvoke( id + "終了" );
});
} );
t.IsBackground = true;
t.Start();
}

/// プールスレッドを使った非同期処理。待たない。
private void button2_Click( object sender, EventArgs e )
{
Parallel.For( 0, 20, id => {
AddMsgUseBeginInvoke( id + "開始" );
System.Threading.Thread.Sleep( 300 );
AddMsgUseBeginInvoke( id + "終了" );
} );
}

/// プールスレッドを使った同期
private void button3_Click( object sender, EventArgs e )
{
Parallel.For( 0, 20, id => {
AddMsgUseInvoke( id + "開始" );
System.Threading.Thread.Sleep( 300 );
AddMsgUseInvoke( id + "終了" );
} );
}

/// BeginInvokeを使ったTextBox再描画処理
private void AddMsgUseBeginInvoke( string msg )
{
IAsyncResult sync =
textBox1.BeginInvoke( new MethodInvoker( () => {
textBox1.Text += msg + " - 1" + "\r\n";
textBox1.Text += msg + " - 2" + "\r\n";
textBox1.Text += msg + " - 3" + "\r\n";
textBox1.Text += msg + " - 4" + "\r\n";
textBox1.Text += msg + " - 5" + "\r\n";
textBox1.Refresh();
}
) );
//ここのコメントを外すとInvokeと同じ
//textBox1.EndInvoke( sync );
}

/// Invokeを使ったTextBox再描画処理。
private void AddMsgUseInvoke( string msg )
{
//デッドロックする
//スレッドプールが足りずに発生している?
textBox1.Invoke( new MethodInvoker( () => {
textBox1.Text += msg + " - 1" + "\r\n";
textBox1.Text += msg + " - 2" + "\r\n";
textBox1.Text += msg + " - 3" + "\r\n";
textBox1.Text += msg + " - 4" + "\r\n";
textBox1.Text += msg + " - 5" + "\r\n";
textBox1.Refresh();
}
) );
}
検証していませんが、メッセージ送りすぎであふれたのかもしれませんね。

推測材料など(未確認事項を含む):
・TextBox.Text への代入は内部的に再描画が発生していた(はず)。
・Invoke はメッセージベースで実装されていて、処理依頼を送ってから処理完了を待つので、メッセージがあふれると止まったままになる。
・Invoke から帰ってこないと Parallel.For は完了しないので、UI スレッドはメッセージループに戻らず、デッドロック状態に。


個人的には BeginInvoke/Invoke をシビアな条件下で使うことはおすすめできません。
ただ、それの裏付け、確証といわれると確固たるものを出せないのが実情ですが…。
■No30520に返信(unripe01さんの記事)

SyncLockを入れてみるとか?
Azuleanさん 書き込みありがとうございます。

>Invoke はメッセージベースで実装されていて
メッセージベースだというのはどこかで見た記憶があります(同じく、ハズ)
内部ではSendMessageなんでしょうかね・・・(逆にBeginInvoleは、PostMessageか。)


> ・Invoke から帰ってこないと Parallel.For は完了しないので、UI スレッドはメッセージループに戻らず、デッドロック状態に。
可能性としては、了解しました!!ありがとうございます。
検証方法が難しいですが。
あと、BeginInvokeでの呼び出しについては、処理を放置すると一応は処理が完了しますが、処理中にTextBoxをクリックしたり、マウスで再描画させるような行為(カーソル移動させようとしたり)を行うと止まります。
これは、EndInvokeを呼び出して同期させてないから、何らかのロックなんだろうとは推測できてるのですが、どういうフローなのかがさっぱり・・・
同期がとれていないという点であれば、New Threadしてる方も同じハズ・・・という理解です。


> 個人的には BeginInvoke/Invoke をシビアな条件下で使うことはおすすめでき>ません。
ですね。。ありがとうございます。
shuさん 書き込みありがとうございます。

> SyncLockを入れてみるとか?

試しに、AddMsgUseInvokeメソッド内部のtextBox1.Invokeを
lockで囲ってみましたが、1スレッドの開始1-5が実行された瞬間に
フリーズ状態になりました・・・
当然、lockオブジェクトはクラスフィールドとして宣言しています。

うまくロックされ、invokeが1スレッドずつ動くと思ったんですけど・・・
■No30535に返信(unripe01さんの記事)

とりあえず以下だと大丈夫そうなのでやはりTextBox.Textの書き換えに問題がありそうです。

private StringBuilder _msg;

private void button1_Click(object sender, EventArgs e)
{
    _msg = new StringBuilder();
    Parallel.For(0, 20, id =>
    {
        AddMsgUseInvoke(id + "開始");
        System.Threading.Thread.Sleep(300);
        AddMsgUseInvoke(id + "終了");
    });
    textBox1.Text = _msg.ToString();
}

private void AddMsgUseInvoke(string msg)
{
    lock (_msg)
    {
        //デッドロックする
        //スレッドプールが足りずに発生している?
        _msg.AppendLine(msg + " - 1");
        _msg.AppendLine(msg + " - 2");
        _msg.AppendLine(msg + " - 3");
        _msg.AppendLine(msg + " - 4");
        _msg.AppendLine(msg + " - 5");
    }
}
> とりあえず以下だと大丈夫そうなのでやはりTextBox.Textの書き換えに問題がありそうです。
>
> private StringBuilder _msg;
>
> private void button1_Click(object sender, EventArgs e)
> {
> _msg = new StringBuilder();
> Parallel.For(0, 20, id =>
> {
> AddMsgUseInvoke(id + "開始");
> System.Threading.Thread.Sleep(300);
> AddMsgUseInvoke(id + "終了");
> });
> textBox1.Text = _msg.ToString();
> }
>
> private void AddMsgUseInvoke(string msg)
> {
> lock (_msg)
> {
> //デッドロックする
> //スレッドプールが足りずに発生している?
> _msg.AppendLine(msg + " - 1");
> _msg.AppendLine(msg + " - 2");
> _msg.AppendLine(msg + " - 3");
> _msg.AppendLine(msg + " - 4");
> _msg.AppendLine(msg + " - 5");
> }
> }
shu さん検証いただいて本当にありがとうございます。
ただ、その例ですと、並列処理のすべての終了を以てUIスレッドに結果反映となるので、並列処理中の各スレッドの終了通知を受け取ったことにならないですね。

もう少し調べてみます。
投稿者です。

皆様、色々と賢察いただいてありがとうございます。
一応、自己解決というか、一つの結論が分かりましたので投稿します。

MSのサイトにまさにデッドロックとなるソースコードが記載されていました。
私が書いていたコードとほぼ一緒でした。

http://msdn.microsoft.com/ja-jp/library/dd997392.aspx
>引用
>次の例では、並列ループが実行されている UI スレッドは、すべての反復処理が完了するまでループによってブロックされます。ただし、このループの反復処理がバックグラウンド スレッドで実行されると (For と同じ処理を行う)、Invoke の呼び出しによって UI スレッドにメッセージが送信され、そのメッセージが処理されるのを待機することになります。For を実行中の UI スレッドはブロックされているので、メッセージが処理されることはなく、UI スレッドでデッドロックが発生します。

>private void button1_Click(object sender, EventArgs e)
>{
> Parallel.For(0, N, i =>
> {
> // do work for i
> button1.Invoke((Action)delegate { DisplayProgress(i); });
> });
>}

>タスク インスタンス内でループを実行することによってこのデッドロックを避ける方法を次の例に示します。UI スレッドはループによってブロックされず、メッセージを処理できます。

>private void button1_Click(object sender, EventArgs e)
>{
> Task.Factory.StartNew(() =>
> Parallel.For(0, N, i =>
> {
> // do work for i
> button1.Invoke((Action)delegate { DisplayProgress(i); });
> })
> );
>}

>引用おわり

ざっくりですが、
UIスレッドをブロックするステートメント内で、UIスレッドにメッセージを投げて待機するような処理を行うな。(UIスレッドはメッセージを処理できる状態にしておけ)
ってことですね。

というわけで、引用コードはTask.Factoryを使ってますが、
New Threadを使うとロックしない理由もこれと同様の理由だと思います。
(invokeかBeginInvokeのどちらを使うかは別として)

MS推奨のコードでいくと
タスクインスタンス内からInvokeを呼び出してUI側に通知するというのがお作法のようですので呼び方としても以下のコードで解決とさせていただきます。

private void button1_Click( object sender, EventArgs e )
{
//マニュアルスレッド・バックグラウンドでパラレル処理実行
var t = new Thread( () => {
Parallel.For( 0, 20, id => {
AddMsgUseInvoke( id + "開始" );
System.Threading.Thread.Sleep( 2000 );
AddMsgUseInvoke( id + "終了" );
} );
} );
t.IsBackground = false;
t.Start();
}

private void AddMsgUseInvoke( string msg )
{
textBox1.Invoke( new MethodInvoker( () =>
{
textBox1.Text += msg + " - 1" + "\r\n";
textBox1.Text += msg + " - 2" + "\r\n";
textBox1.Text += msg + " - 3" + "\r\n";
textBox1.Text += msg + " - 4" + "\r\n";
textBox1.Text += msg + " - 5" + "\r\n";
textBox1.Refresh();
}
) );
}

MSに記載があったにも関わらず、見つけることができておらず
色々と助言いただきありがとうございました。
解決済み!を忘れていました。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板