気楽なソフト工房

プログラミングについていろいろな記事を書いています。



mykonos2008

Author:mykonos2008
システムエンジニアとして働いている30代の会社員です。
仕事や趣味でプログラムを書いている方の役に立つ記事を書いていきたいと思っています。
ご意見、ご感想はこちらまで
If you are an english speaker,Please visit my english blog.

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
このブログでは、ソフトウェアの開発日記の他にソフトの開発途中で得たC#のTipsを紹介していきたいと思っています。
初回の今回は、WindowsフォームのパフォーマンスについてのTipsです。

日ごろ、いろいろなアプリケーションを使用している時、画面が真っ白になってタスクバーに「応答なし」と表示されてしまったりすることを見かけると思います。
落ちちゃったのかなと思っていると突然また動き出したりする場合もあると思います。

この時、アプリケーションの内部では、決して処理が止まってしまっているわけではありません。順に説明していきます。

応答なしの原因


Windowsは常にコンピュータ上で起こった様々な出来事をそれに関連しているアプリケーションにメッセージとして伝えるということをしてくれています。
例えば、ユーザがアプリケーション内のボタンをクリックしたり、重なっていたウィンドウが閉じられたことによって
アプリケーションのフォームが前面に出てきた場合などがその例です。

そして、各アプリケーションは、Windowsから送られてくるこのメッセージを処理することにより、ソフトウェアとして機能しているのです。
しかし、このメッセージが長い間処理されずに無視されると、ユーザから見ると、アプリケーションが止まってしまったように見えるのです。
画面の描画もメッセージを受けて行われるので、画面が真っ白になるのも、描画しろというメッセージが処理されないとことが原因なのです。

では、なぜメッセージが処理されなくなってしまうのかを説明します。
Windowsから送られるメッセージは各アプリケーション毎のキューに入れられます。そして、各アプリケーションはキューからメッセージを1つずつ取り出して処理をしていきます。
このキューの中身を確認し、メッセージがあれば取り出して処理をする一連の処理を「メッセージループ」といいます。

Windowsの各アプリケーションは、必ずメッセージループを行う必要があります。C#のWindowsフォームアプリケーションの場合、「Application.Run()」を呼び出すと、
呼び出したスレッドを使ってメッセージループが開始されます。
(「Visual Studio」ではWindowsフォームプロジェクトのProgram.csに自動的にApplication.Run()が記述されています。)

Application.Run()呼び出し後、呼び出したスレッドがメッセージの取り出しとメッセージの処理を行うことになりますが、
この時点では、1つのスレッドがメッセージの取り出しと、処理を行っているため、1つのメッセージの処理に時間がかかると、他のメッセージが処理されない状態になってしまいます。

例えば、私が現在開発しているYoutubeビューワーのようにWebAPIに接続してデータを取得してくるようなアプリケーションでは、WebAPIから結果が返されるまで待機するので、
その間、他のメッセージが処理されなくなってしまうのです。

これが、応答なしの原因です。

応答なしの対策


応答なしを回避する一番簡単な方法は、時間がかかる処理の途中で、「Application.DoEvents()」をコールして、キューにあるメッセージを全て処理するようにすることです。
しかし、この方法だと、1行の処理で時間がかかる処理があった場合や、キューの中に同じく時間のかかるメッセージが有った場合、同じ結果になってしまいます。

最も良い方法は、メッセージの処理をメッセージループを行っているスレッド以外の他のスレッドにやらせることです。
メッセージループを行っているスレッドはキューからメッセージを取り出したら、その処理を行う他のスレッドを起動し、自身はメッセージループを継続するのです。

こうすることによって、キューの中に処理待ちのメッセージが溜まっていくことを回避することが出来ます。

スレッドの起動方法


C#にはスレッドを起動する方法が3つ用意されています。

1つ目は直接、スレッドを起動する方法です。


     private void _searchBtn_Click(object sender, EventArgs e)
     {
         try
         {
             Thread thread = new Thread(new ThreadStart(ExecuteSearch));
             thread.Start();

         }
         catch {}
     }

     private void ExecuteSearch()
     {
        //処理
     }


この方法だとメッセージが処理される度にスレッドが生成され、破棄されるため、オーバーヘッドが生じます。
以下のようにすることでFrameworkに備えられているスレッドプールの仕組みを利用することができます。


     private void _searchBtn_Click(object sender, EventArgs e)
     {
         try
         {
             ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteSearch));
         }
         catch {}
     }

     private void ExecuteSearch(Object state)
     {
        //処理
     }


3つ目は非同期デリゲートを使用する方法です。


     private delegate void ExecuteSearchDelegate();

     private void _searchBtn_Click(object sender, EventArgs e)
     {
         try
         {
             ExecuteSearchDelegate dlgt = new ExecuteSearchDelegate(ExecuteSearch);
             dlgt.BeginInvoke(new AsyncCallback(ExecuteSearchCallback), dlgt);
         }
         catch {}
     }

     private void ExecuteSearch()
     {
        //処理
     }

     //処理が終了した際のコールバックメソッド
     private void ExecuteSearchCallback(IAsyncResult ar)
     {
        //処理
     }


内部的には2番目と同じく、スレッドプールのスレッドが使用されます。

個人的には、記述が簡単なので、2番目の方法が好きです。

別スレッドからのControlへのアクセス


Windowsフォームアプリケーションで、別スレッドを使用する場合の考慮点があります。Windowsフォームの各コントロールのメソッドやプロパティはスレッドセーフで無いため、
コントロールを作成したスレッドと別スレッドからアクセスすると予期しない動作をする可能性があります。(デバックモードでは100%例外が発生するようになっています。)

通常、コントロールの作成はメッセージループをおこなっているスレッドが行うことになるため(別スレッドで作成するととても面倒くさいことになります)、
前述のように別スレッドに処理させた結果をコントロールに反映させたい場合、問題になります。

これを回避するために、Controlクラスには、Controlを作成したスレッドを呼び出して、そのスレッドに指定した処理を実行させるためのメソッド「Invoke()」が提供されています。


     private void _searchBtn_Click(object sender, EventArgs e)
     {
         try
         {
             ThreadPool.QueueUserWorkItem(new WaitCallback(ExecuteSearch));
         }
         catch {}
     }

     private void ExecuteSearch(Object state)
     {
        //処理

        Invoke((MethodInvoker)delegate()
        {
            //コントロールにアクセスするコード
        });
     }

ちなみに筆者が今回、開発しているYoutubeビューワーでは、このInvoke()処理を行っている途中に動画の再生が一瞬止まってしまうように見えるため、
出来るだけInvoke()の中身を細かく切り、 どうしても切れない場合は、その中で「Application.DoEvents()」を呼ぶという手間をかけています。

コメント

大変役に立ちました。

Windowsメッセージのポンプについて、どのように対応したら良いのか困っておりました所、丁度、ここで具体的に説明がされており、とても助かりました。
ありがとうございました。

Re: 大変役に立ちました。

お役に立てて良かったです。今後ともよろしくお願いします。
> Windowsメッセージのポンプについて、どのように対応したら良いのか困っておりました所、丁度、ここで具体的に説明がされており、とても助かりました。
> ありがとうございました。

コメントの投稿

管理者にだけ表示を許可する

トラックバック

http://csfun.blog49.fc2.com/tb.php/6-4b54b970

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。