読者です 読者をやめる 読者になる 読者になる

亀岡的プログラマ日記

京都のベッドタウン、亀岡よりだらだらとお送りいたします。

メッセージループをアプリケーション内で複数作ってみる

非同期プログラミングはここ最近ですっかりメジャーになりましたね。まぁ最近のC#系統の非同期処理の書き方の楽になり方はやはりすんげえです。あの威力を思い知ってしまうと、どんどん書きたくなるものですよね。

さて、最近の非同期プログラミングの方向性として強いのはUIスレッドから重たい処理を取り払う、という系統のものですよね。
例えば、こんなの。

private void button_Click(object sender, EventArgs e)
{
    var waitProgressForm = new WaitProgressForm();
    waitProgressForm.Show();

    //何か長い処理
    Task.Factory.StartNew(() => Thread.Sleep(5000)).Wait();

    waitProgressForm.Hide();
}

これでUIスレッドが固まることなく、処理を続けていくことができます。やったね。

はてさて。本当にこれでいいんでしょうか。

UIスレッドで回さないといけない処理だって有るよね

物事には面倒臭いことが色々あります。今回の例では、Taskの中でぶん回す処理は暗黙的にスレッドセーフであることを前提としています。でもスレッドセーフじゃない処理も結構あります。主にUIを触る処理ですね。

UIを触る処理自体を重たくするのが悪いは悪いのですが、それでも時間がかかる場面はあります。*1

その場合は、まぁ仕方ないのでこう書く・・・?

    //何か長いUI処理
    Task.Factory.StartNew(
        () => Thread.Sleep(5000),
        new CancellationToken(),
        TaskCreationOptions.None,
        TaskScheduler.FromCurrentSynchronizationContext()).Wait();

    waitProgressForm.Hide();

いやいやいや。これだと描画系が止まってしまうので待ち状態表示がちゃんと動きませぬ。ぐぬぬぬ。

とりあえず、待ち状態フォームくらいは動かそう。

んでですね。そもそも親フォームが描画止まっちゃうのはさすがに仕方ないです。でも待ち状態表示くらいはしたいのですよ。

ではどうするか、というと、別スレッドでUIフォームを表示しちゃえばいいのです。こんな感じに。

     var waitProgressForm = new WaitProgressForm();

     var task = Task.Factory.StartNew(() => waitProgressForm.ShowDialog());

     //何か長いUI処理
     Thread.Sleep(5000);

     waitProgressForm.BeginInvoke((MethodInvoker)(waitProgressForm.Close));

なんとも気持ち悪い感じですが、これでちゃんと動いてくれます。「UIスレッドじゃないのにUIスレッド作っていいの!?」と思はないでもないですが、これはむしろUIスレッド(というかメッセージループ)が2つある状態になっているわけですね。

んで、逆にUIスレッドといえども、待ちFormは別のスレッドで動いてますのでBeginInvokeしているわけですね。

ShowDialogは暗黙的にメッセージループを作ってくれます。
んでもそれだと下触れませんよね。いや、下がビジーなんだからそれでも問題ないとは思うのですが。まぁいちおう、Showでもできます。ただし、Showだけではだめで、以下の様な感じにします。

     var waitProgressForm = new WaitProgressForm();

     var task = Task.Factory.StartNew(
         () =>
             {
                 Application.Run(waitProgressForm);
             });

     //何か長いUI処理
     Thread.Sleep(5000);

     waitProgressForm.BeginInvoke((MethodInvoker) (waitProgressForm.Close));

Application.Runによりメッセージループを明示的に開始してやる必要があるわけですね。

WPFだと話はすこし複雑に。

ちなみに、WPFだと話はすこしややこしくなります。

まず、明示的にSTAのスレッドで呼び出すことを要求するので、ThreadPoolを用いるTaskによる呼び出しができません。
なので、頑張ってThreadを作って、アパートメントを設定してやります。

それからもう一つのポイントはDispatcherFrameです。これもメッセージループを回す役目を持っていて、Continueプロパティによりオンオフの制御ができるという優れ物です。これを表示直後に呼び出すことでメッセージループが回り始めることになります。これでちゃんとUIが動いてくれます。

      SubWindow subWindow = null;
      DispatcherFrame dispacherFrame = null;

      var thread = new Thread(
          () =>
              {
                  dispacherFrame = new DispatcherFrame(true);
                  subWindow = new SubWindow();
                  subWindow.Show();
                  subWindow.Closed += (o, args) =>
                                          {
                                             dispacherFrame.Continue = false;
                                          };
                  Dispatcher.PushFrame(dispacherFrame);
              });
      thread.SetApartmentState(ApartmentState.STA);
      
      thread.Start();

      Thread.Sleep(5000);
      
      dispacherFrame.Dispatcher.BeginInvoke( new Action(subWindow.Close) );

ちなみにWPFだと別スレッドからShowDialogしたあとに、そのShowDialogしているスレッドのDispatcherを取る方法を僕はまだ見つけられていません。

んで、そもそもどうなのよ。

まぁ、こんな感じで、とりあえず「技術的には可能なんだな」というのは分かっていただけたかと思います。
ただ、この「複数のメッセージループが回っている」っていう状態そのものがなんだかキモチワルイ気もします。

そんなわけで、そこの是非について色々調べてみたのですが、

Using message loops in each thread is perfectly fine; Windows is optimized for this scenario.

I think it's perfectly fine do create multiple message loops on different threads. The only thing to watch out for is when dealing with 3rd party UI toolkits, they sometimes store handles as static (instead of ThreadStatic) members and if you have multiple UI threads in your application, it will have problems (in my case, I found that the menu / toolbar keyboard accelerators did not work properly).

とまぁ、完全にダメ、というわけでは無さそうです。基本的にはOKなイメージ。

もちろん、本来こういう状況になるのは避けるべきなんでしょうけれど、まぁ背に腹は変えられない時もあるわけですしね。

*1:特にWindowsFormではWPFのようにDataTemplateなどを用いたUI生成の遅延ができないので結構良く見る印象があります