スレッドの使い方

戻る

32ビットアプリケーションを作成するための環境が整い,16ビット アプリケーションを新規に作成することは少なくなってきているのではないかと 思われます.16ビットアプリケーションと異なり,32ビットでは完全な (プリエンティブな)マルチスレッド・マルチタスクでプロセスが実行できるように なっています.
しかし,最近スレッドの使い方を誤ることによるトラブルをよく見掛け ます.スレッドを使い方を間違えるとシステムのパフォーマンスを低下させたり, システムリソースを不必要に消費させてしまいます.また,各スレッドで共有する システムリソースを,いずれかのスレッドが勝手に破棄したり,スレッドが何かの処理 を行っている途中でプロセスが終了してしまうと何が起こるか分かりません.
そこで,基本的なスレッドの扱いについてまとめてみたいと思います.

19-1 スレッドとは

いまさらスレッドを説明することもないかもしれませんが,16ビット アプリケーションなどを主に作成されてきた方には馴染みのない機能ではないかと 思います.そこで,スレッドとはいったい何なのかを簡単に説明します.
スレッドはシステムがCPUを割り当てる基本単位です.Windows95やWindowsNT上で 動作するアプリケーション,つまりプロセスは必ず1つのスレッドを持っています. つまり,なにも意識しなくてもスレッドを生成している訳です.
UNIXでのマルチプロセスでは,1つのコードに複数のプロセスIDとデータ領域 を用意して別々にCPUを割り当てて動作させることができるようになっています.
これが fork関数の役目です.fork関数で複製されたプロセスは,プロセスIDで, 「本家」のプロセスか,「分家」のプロセスかを識別して別々の動作を行うように します.しかし,スレッドは1つのプロセスの中に複数存在することができます.
Windows でのスレッドは,まずプロセスが生成されたときに生成されます.つまり, WinMain や main 関数が1つめのスレッドとなります.2つめ以降のスレッドは, Win32 API CreateThread で指定した関数を起点として,動作を開始します.
このため,スレッドは関数単位で独立して動作するように見えます. スレッドを識別するにはスレッドIDを利用し,複数のスレッドから利用する関数は 必要な場合を除いてグローバル変数を利用せずにスタック(ローカル変数)を利用 します.各スレッドは,別々にスタックを持っているため,ローカル変数を利用して おけば,同時に複数のスレッドが同じ関数を呼び出しても正常に動作させることが 可能になります.
スレッドは,UNIXで一般的に利用されている fork関数と比較すると処理の組み立て が大変簡単で小回りの利く機能であるといえます.しかし,使い方を間違えると デバッグを困難にしてしまうこともあります.しかし,16ビットアプリケーションの ようにイベントのみで動作を切り分ける場合と比べ,処理がシンプルでわかりやすい 構造にすることができます.

19-2 スレッドの一生

●スレッドの誕生
通常,スレッドを生成するときの処理に注意を払いますが,プロセスが終了するときの タイミングについては,意外と見落とされがちです.スレッドを生成するには, いくつかの方法があります.
Win32 API ... CreateThread
Cランタイムルーチン ... _beginthread, _beginthreadex
MFC ... AfxBeginThread
ここで注意しなければならないのは,スレッドの中でCランタイムルーチンを利用 する場合,Win32 API CreateThread を利用せず,_beginthread を利用する必要が あります.CreateThread で生成したスレッドの中で一部のCランタイムの関数を 利用し,スレッドを終了させるとメモリリークを生じます.
また,Cランタイムのスレッド関数とWin32 API のスレッド関数を混在させて利用する とトラブルが発生する恐れがありますので,注意が必要です.
MFCでスレッドを生成するときには,AfxBeginThread を利用する必要があります. また,CWinThread 派生オブジェクトを構築してそれから CreateThread を呼び出す ことにより,スレッドの実行と終了を繰り返す場合に,CWinThread オブジェクトを 再利用することができます.
MFCアプリケーションで _beginthread を利用することもできますが,この関数で生成 したスレッドではMFCのメソッドを利用できません.

●スレッドの最期
使い終わったスレッドは,return で終了するか,次の関数で終了させます.
プロセスが終了すれば,そのプロセス上で動作しているスレッドも自動的に終了 されます.Win32 API CreateThread でスレッドを生成したときに取得するスレッド ハンドルは Win32 API CloseHandle で確実に解放しておく必要があります.
このハンドルは,自動的に解放されませんので,メモリリークの原因になります. _beginthread でスレッドを生成した場合は,スレッドがreturn で終了した場合や, _endthread を呼び出して明示的にスレッドを終了させればハンドルの解放は自動的に 行われます.なお,LIBCMT.LIB をリンクしたアプリケーションで, Win32 API ExitThread を利用してスレッドを終了させると,実行時のシステムで 割り当てられているリソースを解放できなくなりますので注意が必要です.
Win32 API ... ExitThread
Cランタイムルーチン ... _endthread, _endthreadex
MFC ... AfxEndThread

●スレッドは「他殺」でなく「自然死」または「自殺」で終了しなければならない
他のスレッドから Win32 TerminateThread などを利用してスレッドを強制終了 させると,メモリリークや予期せぬトラブルが発生する恐れがあります.
したがって,他のスレッドからスレッドを終了させる必要がある場合,イベントや スレッドメッセージを送信して,スレッド自身のシャットダウン処理を実行させてから 自発的に終了するように処理を組み立てる必要があります.
スレッドの終了コードは,次の関数で取得します.
Win32 API ... GetExitCodeThread
なお,Win32 API GetExitCodeThread でスレッドが実行中かどうかを知ることも できます.

リスト19-1:スレッドが終了するまで待つ

    /* スレッドが終了するまで待つ */
    do{
        Sleep(500);
        GetExitCodeThread(hThread, &dwExitCode);
    }while(dwExitCode == STILL_ACTIVE){

●子スレッドは確実に終了させてから親スレッド(プロセス)を終了させる
親スレッドで取得したリソースを子スレッドで利用している場合,子スレッドの処理が 完全に終了させないうちに親スレッドがリソースを解放して終了してしまうと, アプリケーションが異常終了する可能性もあります.困ったことに,このような処理は 終了させるタイミングによって正常に終了することもありますので,トラブルの原因の 特定が困難な場合があります.

●複数のスレッドからスタティックデータを扱う場合
プロセス間でデータ領域を共有する場合は,メモリマップドファイルなどを利用する 必要がありますが,スレッドはプロセス上のデータを自由に参照することができます. このため,プロセス内で定義されたグローバルデータは各スレッドで参照して処理を 行うことができます.複数のスレッドから共通のデータをアクセスする必要がとき, 基本的にはクリティカルセクションなどを利用して同時にアクセスできないようにして おいた方がよいでしょう.しかし,複数のスレッドが同時に書き込みを行っていない ことや,変数が32ビット数値1つ(文字列や配列でない)の場合,排他制御を 行わなくても安全な場合もあります.しかし,拡張性を考えると速度的に問題が なければ,できるだけ安全な処理にしておいたほうがよいでしょう.

●スレッドローカルな記憶域
スレッドはプロセスの仮想アドレス空間とグローバル変数を共有します.
しかし,場合によっては各スレッドにローカルな静的記憶域があった方がよいことも あります.当然のことですが,スレッド関数のローカル変数は関数を実行する 各スレッドにローカルが,スレッドがほかの関数を呼び出す場合に,その関数が使う 静的変数やグローバル変数の値はどのスレッドでも同じになります.
このような状態を回避するために,クリティカルセクションで同時に複数のスレッドが 実行できないようにすることができますが,TLS(スレッドローカル記憶域)を利用して, プロセスの任意のスレッドがスレッドごとに異なる値を格納・取得できるようにする ことができます.TLSは一般的に次のような手順で利用します.
(1) プロセスやDLLの初期化時に Win32 API TlsAlloc でTLSインデックスを 取得する
(2) TLSインデックスを使わなければならないスレッドは,動的記憶域を割り当てて から,Win32 API TlsSetValueで,その記憶域を指すポインタとインデックスを 関連付ける
(3) スレッドは,記憶域をアクセスするとき,TLSインデックスを指定して Win32 API TlsGetValue を呼び出してポインタを取得する
(4) スレッドは,TLSインデックスに関連付けられている動的記憶域が不要になったら 記憶域を解放する
(5) すべてのスレッドがTLSインデックスを使い終わると,Win32 API TlsFree で インデックスを解放する
Visual C++ を利用するときには,これらの関数を利用する代わりに __declspec(thread) を利用することができます. __declspec(thread)はスタティックデータのみ利用することができます. 例えば,
__declspec(thread) int tls_iCounter; /* カウンタ */
のように定義します.

19-3 スレッド間の協調

複数の処理を同時に動作させ,ある処理を実行するために他の処理が終了するまで 待ち合わせる必要がある場合や,複数のスレッドから同時にアクセスできないリソース に対して,1度に1スレッドでしか処理を行えないようにしたい場合など,スレッドの 同期制御は,マルチスレッドアプリケーションでは大変重要になります.

●クリティカルセクション
◆クリティカルセクションの初期化と削除
1つのアプリケーションで動作する複数のスレッド間の同期をとるもっとも簡単な 方法は,クリティカルセクションです.例えば,フラットサンクを利用して16ビット コードを呼び出す場合,その処理を複数のスレッドで利用する場合には必ず必要に なります. クリティカルセクションは,CRITICAL_SECTION構造体単位で管理されます. 1つのCRITICAL_SECTION構造体で複数の処理を制御すると, 他の処理で LeaveCriticalSection が呼び出されるとその他の LeaveCriticalSection も同じスレッドでロックされます.
クリティカルセクションを利用するには,まず Win32 API InitializeCriticalSection で初期化を行い,利用を終了したら DeleteCriticalSection で削除する必要が あります.
初期化を行わなかったり,初期化後に削除して,Win32 API EnterCriticalSection や Win32 API LeaveCriticalSection を呼び出すと例外が発生し,アプリケーションが 異常終了してしまいます.つまり,main関数でクリティカルセクションの初期化・削除 を行い,スレッド内でEnterCriticalSection, LeaveCriticalSection を呼び出して いる場合,スレッドを停止させてからクリティカルセクションを削除しなければ, アプリケーションが例外を起こす恐れがあるということです.

リスト19-2:同じクリティカルセクションを複数の処理で利用した

    CRITICAL_SECTION  cs;


処理A()
{
    EnterCriticalSection(&cs);
    処理;
    LeaveCriticalSection(&cs);
}

処理B()
{
    EnterCriticalSection(&cs);
    処理;
    LeaveCriticalSection(&cs);
}


スレッドA()
{
    処理A()   ← (1) 処理Aを実行開始
}

スレッドB()
{
    処理B()   ← (2) スレッドAが処理Aを実行中に処理Bを実行開始
}

         スレッドAが処理Aの実行を開始した直後にスレッドBが処理Bを実行
         した場合,スレッドAが処理Aを終了するまで,スレッドBは待ち状態
         になる.

◆クリティカルセクションの利用
クリティカルセクションオブジェクトを初期化すると,排他制御を行うことができる ようになります.
クリティカルセクションは,同時に複数のスレッドが実行できない処理を同時に1つの スレッドしか実行できなようにすることができます. フラットサンクを利用して16ビットコードを呼び出すときはもちろん,関数内で ローカルなスタティック変数やグローバル変数を操作するときなど,いろいろな用途が 考えられます.
Win32 API EnterCriticalSection が呼び出されると最初に,このAPIを呼び出した スレッドの処理のみが通過できるようになります.
他のスレッドが EnterCriticalSection を呼び出すと,最初に呼び出したスレッドが Win32 API LeaveCriticalSection よ呼び出すまで,他のスレッドの処理は停止して しまいます.つまり,ある関数でスレッドが EnterCriticalSection を呼び出し, LeaveCriticalSection を呼び出さずに処理を終えると,その処理は他のスレッドが 処理を行えなくなるどころか,他のEnterCriticalSection を呼び出したまま停止して しまいます.このような状態になっても,最初のスレッドは同じ処理を何事もなかった かのように処理を行うことができます.
このような状態は,関数の異常系処理で return エラーコード; で終了するときに, LeaveCriticalSection を実行し忘れているときに発生します.このような状態に ならないようにするためには,__try/__finally などを利用して,Enter すると必ず Leave するようにしておく必要があります.

リスト19-3:確実に LeaveCriticalSection を実行させる

処理()
{
    __try{
        EnterCriticalSection(&cs);

        逐次再実行可能な処理A(再入不可)
         if(エラー){
           __leave;
        }
        逐次再実行可能な処理B(再入不可)
    }
    __finally{
        LeaveCriticalSection(&cs);
    }
}

●セマフォ・ミューテックス
セマフォやミューテックスも同期をとるために利用する機能です.
セマフォは,0からある値までのカウント値を持ち,カウントが0以上のときに シグナル状態になり,カウンタが0のときに非シグナル状態になります. ミューテックスは,いかなるスレッドにも所有されていないときにシグナル状態に なり,所有されているときに非シグナル状態になります.一度に1つのスレッドだけが ミューテックスを所有することができるため,カウンタが1のセマフォと見ることが できます.セマフォやミューテックスは,クリティカルセクションとは異なり, プロセス間の協調をとるために利用することができます.
(* 拙著 Win32サブルーチンズ 第3章 を参照.)
しかし,クリティカルセクションと比べると処理が重いため,速度を要求する部分での 利用には注意が必要です.ミューテックスは,同じアプリケーションを同時に複数起動 させないようにするための手段として利用されることが多いようです.

19-4 スレッドのプライオリティ

スレッドやプロセスには,実行優先順位を持っています.
サービスのようにユーザから直接操作しなくても動作するアプリケーションは低く, ウィンドウアプリケーションのようにユーザが直接操作することで動作する アプリケーションは高くすることで,システム全体を円滑に動作させることができる ようになっています.
アプリケーションは利用される用途によって,プロセスやスレッドの優先順位を操作 することができます.しかし,すべてのアプリケーションが自分の好きなように スレッドやプロセスの優先順位を上げてしまうとシステムが正常に動作できなくなって しまいます.そこで,権限のないアプリケーションはスレッドの優先順位を低くする ことしかできないようになっています.

●スレッドの優先順位
スレッドは,スレッドの優先順位とスレッドを所有するプロセスの優先順位クラスに より決められる,基本優先順位レベルを持っています.オペレーティングシステムは, 基本優先順位レベルから,次のCPUタイムスライスをどのスレッドに与えるかを決定 します.スレッドは,ラウンドロビン方式で各優先順位レベルに応じて スケジューリングされます.システムは,高いレベルを持つ実行可能スレッドがない ときだけ,低いレベルを持つスレッドへのスケジューリングを行います.
優先順位を操作するときには,高い優先順位クラスを持つスレッドが利用可能な CPUサイクルのすべてを使わないよう注意する必要があります.
11を超える基本優先順位レベルを持つスレッドは,システムの操作に影響を与えます. REALTIME_PRIORITY_CLASSを指定すると,ディスクキャッシュのフラッシュが正しく 行われなくなったり,マウスのハングアップなどの原因となることがあります.
 サービスで,プライオリティを高く設定すると,デスクトップ上のアプリケーション の速度が低下したり,正しく動作しなくなることもあります.システム全体を円滑に 動作させるには,CPU を必要としない処理(待ちなど)はプライオリティを下げること により,他の処理を円滑に実行できるようにすることも必要ではないかと 思われます.

●優先順位の設定
プロセスの入力スレッドのようにリアルタイム性を要求するスレッドには高い優先順位 を設定して,ユーザーに対する応答を確保します.それ以外のCPU時間を消費する スレッドには低い相対優先順位を設定して,必要ならほかの処理にCPU時間を譲れる ようにします.しかし,優先順位の低いスレッドが処理を完了するのを優先順位の高い スレッドが待つときはループで待機せずに,待機関数やクリティカルセクションや Win32 API Sleepなどを使って待機中のスレッドの実行を停止します.
ループで待機すると,ループ自体でCPUを消費してしまい,優先順位の低いスレッドが スケジュールされなくなり,プロセスがデッドロックしてしまう恐れがあります.

表19-1:スレッドの優先順位の設定/取得API
BOOL SetThreadPriority(HANDLE hThread, int iPriority)
指定されたスレッドに対する優先順位値を設定する

引数
HANDLE hThread ... スレッドを識別するハンドル
 THREAD_SET_INFORMATIONアクセス権が必要
int iPriority ... スレッドの優先順位レベル
 THREAD_PRIORITY_IDLE ... IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS プロセスに対しては基本優先順位レベルが1であることを示し, REALTIME_PRIORITY_CLASSのプロセスに対しては基本優先順位レベルが16であることを示す
 THREAD_PRIORITY_LOWEST ... 優先順位クラスに対する標準の優先順位よりも2ポイント低い
 THREAD_PRIORITY_BELOW_NORMAL ... 優先順位クラスに対する標準の優先順位よりも1ポイント低い
 THREAD_PRIORITY_NORMAL ... 優先順位クラスに対する標準の優先順位
 THREAD_PRIORITY_ABOVE_NORMAL ... 優先順位クラスに対する標準の優先順位よりも1ポイント高い
 THREAD_PRIORITY_HIGHEST ... 優先順位クラスに対する標準の優先順位よりも2ポイント高い
 THREAD_PRIORITY_TIME_CRITICAL ... IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASSのプロセスに対しては基本優先順位レベルが15であることを示し, REALTIME_PRIORITY_CLASSのプロセスに対しては基本優先順位

引数
正常終了 TRUE
異常終了 FALSE
int GetThreadPriority(HANDLE hThread)
指定されたスレッドに対する優先順位値を取得する

引数
HANDLE hThread スレッドを識別するハンドル
 THREAD_SET_INFORMATIONアクセス権が必要

引数
正常終了 スレッドの優先順位(SetThreadPriority を参照)
異常終了 THREAD_PRIORITY_ERROR_RETURN

19-5 プログラムについて

非常に簡単なスレッドを利用した処理の例です.
_beginthread で1つのスレッドを生成します.スレッドは3秒に1度処理を実行 します.実行中に[CTRL]+Cを押下するとスレッドの終了してからプロセスを終了 します.スレッドや同期オブジェクトの動作を確認するためのひな形として利用して ください.

●ソースファイル
THREAD.EXE
 THREAD.C (ソースプログラム)
 MAKEFILE (メイクファイル)