[[BORLAND C++]]
INDEX
Section.10 Timer&Thread
ここまでのサンプルを見てきて分かる様に、基本的に、WindowsのGUIアプリは、イベント駆動という方式を採るのだ。即ち、初期設定だけはシーケンシャルに動作し、その後の動作は、ユーザがボタンを押す,キー入力をする等の操作(イベント)が有って、初めて動作する訳である。即ち、ユーザ操作がプログラムの起動の鍵になる訳だが、タイマ等、ハード側からの割り込みで動作するイベントも有るので説明しておこう。タイマについては非常に簡単にコントロール出来るタイマが用意されているので、問題無く使えると思うが、シリアルポート等のシーケンスをプログラムする様な場合、『データの到着をポーリングしながら待つ傍ら、タイマによるタイムアップを監視する』等という処理が必要になるので、タイマとスレッドという処理が必要になってくるのだ。スレッドが使えないと、非常に困った状態になるのである。

  • なぜ、スレッドが必要なのか?
    上述した例の様に、シリアルポート等のシーケンスをプログラムする場合、ユーザがボタンを押下した事をイベントとして、シリアルを出力し、そのレスポンスを待つというプログラムをする事を想定する。この場合、いつまでもレスポンスを待つ訳にはいかないので、タイマを掛けて、タイマのタイムアップを同時に待つという処理を入れるのだが、最初のボタンのイベントで、ポーリングを開始してしまうと、Windowsのプログラム自体が、メッセージループ,特にGetMessage(&msg,NULL,0,0)を実行しないので、タイマのタイムアップイベントどころか、ボタン押下等のイベントもハンドル出来なくなってしまうのだ。即ち、固まった状態である。元々、Windowsアプリは、メッセージループのGetMessage(&msg,NULL,0,0)関数にて、自分宛のメッセージの到着を待つ仕様となっているので、この関数を実行しなければ、イベントが発生しても、メッセージを取得出来ないので、Windowプロシージャも呼ばれないのである。また、悪いことに、この関数は、自分宛のメッセージが無い場合、来るまで待つという動作をするのだ。さて、この状況で、ポーリングを行うにはどうしたら良いのだろうか?ここで、スレッドという処理が必要になってくるのである。更に、スレッドは、何本か同時に持つことが出来るので、特別時間の掛かる重い処理については、別のスレッドを作成して、そちらに処理を任せるという方法も可能なのである。

  • スレッドとは?
    スレッド(Thread)を辞書で引くと、糸という意味が有るのだが、プログラム上で、糸のような動作とは思えないので、何故、この様に呼ぶのかは不明である。ともあれ、スレッドとは、Windowsアプリと並行して、シーケンシャルな動作をするプログラムを起動しておいて、Windowsがメッセージループを監視している間に、シーケンシャルな処理を進める事が出来る仕掛け…と言えば分かるだろうか?即ち、1本のプログラムなのだが、あたかも2本のプログラムが起動していて、マルチタスクで動いている様に動作し、フラグやメッセージ等で通信しながら処理を進める事が出来るのである。

    • スレッドの起動
      スレッドの起動は、スレッドの起動関数にて行う。_beginthread()という関数を使用する場合もあるのだが、ここでは、CreateThread()を使用した。引数は、

      HANDLE CreateThread(
        LPSECURITY_ATTRIBUTES lpThreadAttributes, …不明,大概NULLを指定する
        DWORD dwStackSize, …スタックバイト数,使い方が不明の為0を指定
        LPTHREAD_START_ROUTINE lpStartAddress, …スレッド関数へのポインタ
        LPVOID lpParameter, …スレッド関数の引数
        DWORD dwCreationFlags, …不明,0を指定
        LPDWORD lpThreadId …Thread関数の戻り値を格納する変数ポインタを指定
        );


      である。スレッドを起動したら、スレッドのコントロールはメイン側から行う事になるので、必ず、この関数の戻り値であるスレッドのハンドル(型はHANDLE)を取得して保存しておく事。

      例)DWORD ThreadProg(HWND hTarget)をスレッドプログラムとする場合
      Thand=CreateThread(NULL,0,
          (LPTHREAD_START_ROUTINE)ThreadProg,hwnd,0,ThrdID) ;


    • スレッドの一時停止
      スレッドの実行を、一時停止する場合は、SuspendThread()にて行う。この関数の引数は、CreatThread()関数の戻り値として取得して保存しておいたハンドルを指定すれば良い。但し、現在スレッドが、一時停止状態にある場合に、この関数にて更に一時停止を掛けると、二度と再開しなくなってしまう(なぜか??)ので、状態については、メイン側で管理しなければならない。

      例)
      SuspendThread(Thand) ;


    • スレッドの再開
      スレッドの実行が一時停止状態にある場合、これを再開させるのは、ResumeThread()にて行う。この関数の引数は、CreatThread()関数の戻り値として取得して保存しておいたハンドルを指定すれば良い。これも、一時停止同様,一時停止状態以外の場合に行うと、状態がおかしくなるので、注意しなければならない。

      例)
      ResumeThread(Thand) ;


    • スレッドの終了
      スレッドが実行中又は一時停止中の場合、これを終了させるのは、TerminateThread()にて行う。この関数の引数は、

      BOOL TerminateThread(
        HANDLE hThread, …スレッドのハンドル
        DWORD dwExitCode …スレッドの終了コード
        );


      となっている。スレッドの終了コードについては不明であるが、色々サンプルを見ると、0となっているので、とりあえず0で問題無いと思うが…。

      例)
      TerminateThread(Thand,0) ;


  • タイマ
    スレッドについて、一通り説明が終わったところで、タイマについてである。こちらは非常に簡単に使えるので、スレッド処理以外でも使い道が有るだろう。但し、このタイマは、CPUの性能にも依るが、10ms以下の精度については、保証されていないと聞いた事が有るので、あまり短いタイマは掛けない方が身のためだろう。

    • タイマのセット
      タイマは、SetTimer()関数にてセットする。この関数の引数は、

      UINT SetTimer(
        HWND hWnd, … タイマを扱うウインドウのハンドル
        UINT nIDEvent, … タイマのメッセージID
        UINT uElapse, … タイムアウト値(単位ms)
        TIMERPROC lpTimerFunc … タイマハンドラへのポインタ(使用する場合)
        );


      である。タイマのメッセージIDについては、別途定義しておく必要が有るが、ユーザ定義のメッセージIDとかぶらなければ何でも良い様である。タイマハンドラへのポインタは、今回0(NULL)を指定するが、タイマハンドラを、別途定義すれば、その関数へのポインタを指定しても良い。

      例)500msのタイマを掛ける場合
      SetTimer(hwnd,IDM_TIMER,500,NULL) ;


    • タイマの停止
      タイマの停止は、KillTimer()関数にて行う。この関数の引数は、

      BOOL KillTimer(
        HWND hWnd, … タイマを扱うウインドウのハンドル
        UINT uIDEvent … タイマのメッセージID
        );


      であるが、何れもSetTimerで指定した値をセットすれば良い。

    • タイマのタイムアップ
      タイマがタイムアップすると、タイマを扱うウインドウのハンドルにて指定したWindow(またはダイヤログ)に、WM_TIMERのメッセージイベントが発生するので、ここで、タイムアップ時の処理を行えば良い。但し、SetTimerは、インターバルタイマであり、例えば500msのタイマを掛けた場合、タイマの停止を行わない限り、500ms毎に、永遠とタイムアップイベントが発生する様になっている。もし、単発で掛けたければ、WM_TIMERの処理内で、タイマの終了を行う様にすれば良い。
      尚、タイマのセット時に、タイマハンドラを設定した場合は、WM_TIMERのイベントは発生せず、タイマハンドラが呼ばれるので、そちらで同様の処理を行えば良い。
    追記(5/9)
    ここでは、1本だけのタイマを扱ったので、WM_TIMERの処理は、タイムアップしたタイマの種類を問わずに行う事が出来たが、複数本のタイマを掛けている時は、wParamに、タイマのID(上記例では、IDM_TIMER)が通知されるので、これを判断して、分岐する動作をしなければならないのである。


  • サンプルについて,
    これって、スレッド使う必要有るの??と言われると、きついのだが、大概,スレッドを使うと処理が複雑になる…逆か…処理が複雑になるから、スレッドで並行処理させると言った方が良いかも…という訳で、簡単なサンプルを造るのに、逆に苦労してしまった。普通は、外部の装置と、何らかの通信を行う様な場合、シーケンスの管理と、受信データ解析をスレッド側で行い、メイン側で、タイマやボタン等のイベントを管理する。という使い方をサンプルとして作成すれば、非常に分かりやすいのだが…
    何はともあれ、サンプルの動作としては、

    • メイン側の動作
      500msのタイマを掛けて、タイムアップで現在時刻を取得して表示する。この動作を、ダイヤログ表示開始から、ダイヤログ(アプリ)終了まで継続する。
      また、ダイヤログのスイッチ押下状況により、スレッドの状態を参照して、スレッドの制御を行う。(スレッド開始→スレッド一時停止→スレッド再開→スレッド終了)
    • スレッド側の動作
      スレッド側の動作は、DWORD ThreadProg(HWND hTarget)の中の動作が全てである。このスレッドが動作を開始すると、100msのスリープを挟んでカウンタをカウントアップし、そのカウンタ値を表示するという動作である。まあ、簡易版のストップウォッチと思って貰えれば良い。今回は、スレッド内にダイヤログのテキスト表示部分のハンドルを引き込んで、ダイヤログに直接書く仕様を取ったがPostMessage()を使って、メッセージで通信するという方法を取れば、ダイヤログへの表示は、メイン側でハンドル出来るので、無理にスレッド・プログラムに引数を持たせる必要は無い。尚、スレッドプログラムの中程で、
      if(Counter>=30000) Counter=0 ;
      if(Counter==30001) break ;
      とあるが、これは、コンパイラのワーニング対策で、ちょっとした小細工である。これでは下の行は絶対TRUEにならないから削除してしまおう…などと思うと、コンパイルでワーニングになるのだ。最後のreturn(0) ;を削除するという手も有るが、スレッドプログラムの関数は、DWORDの戻り値を持つ仕様なので、少々気持ちが悪い。特に、スレッドを永久ループで回す仕様の場合は、この様にすればスッキリするのでは無いだろうか?


    テストサンプル011
2002/04/25
HomeSweetHome2
Ozzy's Software