C と Win32 を使用するマルチスレッド

Microsoft C/C++ コンパイラ (MSVC) では、マルチスレッド アプリケーションの作成がサポートされています。 アプリケーションで、ユーザー インターフェイスが応答しなくなるような高負荷な操作を実行する必要がある場合は、複数のスレッドを使用することを検討してください。

MSVC では、複数のスレッドを使用してプログラミングを行う方法がいくつかあります。C++/WinRT と Windows ランタイム ライブラリの使用、Microsoft Foundation Class (MFC) ライブラリの使用、C++/CLI と .NET ランタイムの使用、または C ランタイム ライブラリと Win32 API の使用が可能です。 この記事では、C のマルチスレッドについて説明します。コード例については、C でのマルチスレッド プログラムの例に関するページを参照してください。

マルチスレッド プログラム

スレッドとは、基本的にはプログラム内を通過する実行経路です。 また、Win32 でスケジュールされる、実行の最小単位でもあります。 スレッドは、スタック、CPU レジスタの状態、およびシステム スケジューラの実行リスト内のエントリから構成されます。 各スレッドは、すべてのプロセスのリソースを共有します。

プロセスは、1 つまたは複数のスレッドと、メモリ内のプログラムのコード、データ、およびその他のリソースで構成されます。 一般的なプログラム リソースは、開いているファイル、セマフォ、および動的に割り当てられたメモリです。 プログラムは、システム スケジューラによって、そのスレッドの 1 つに実行制御が渡されたときに実行されます。 スケジューラは、どのスレッドでどのタイミングで実行するかを決定します。 低優先度のスレッドは、高優先度のスレッドがタスクを完了するまで待機しなければならないことがあります。 マルチプロセッサ マシンでは、スケジューラは個々のスレッドを別々のプロセッサに移動して、CPU 負荷を分散させることができます。

プロセス内の各スレッドは、独立して動作します。 スレッドが相互に可視化されていない場合、スレッドは個別に実行され、プロセス内の他のスレッドを認識することはありません。 ただし、共通リソースを共有するスレッドでは、セマフォなどのプロセス間通信手段を使用して、作業を調整する必要があります。 スレッドの同期の詳細については、「マルチスレッド Win32 プログラムの作成」を参照してください。

ライブラリのマルチスレッド サポート

CRT のすべてのバージョンでマルチスレッドがサポートされるようになりました。ただし、一部の関数の非ロック バージョンは例外です。 詳細については、「マルチスレッド ライブラリのパフォーマンス」を参照してください。 コードとのリンクに使用できる CRT のバージョンの詳細については、CRT ライブラリの機能に関するページを参照してください。

マルチスレッドのためのインクルード ファイル

標準の CRT インクルード ファイルでは、ライブラリに実装する際に C ランタイム ライブラリ関数を宣言します。 コンパイラ オプションで __fastcall または __vectorcall の呼び出し規則を指定している場合、コンパイラでは、すべての関数がレジスタ呼び出し規則を使用して呼び出されることを想定しています。 ランタイム ライブラリ関数は C の呼び出し規則を使用し、標準のインクルード ファイルの宣言では、これらの関数への正しい外部参照を生成するようにコンパイラに指示します。

スレッド制御のための CRT 関数

Win32 プログラムはすべて、スレッドを少なくとも 1 つ持っています。 どのスレッドでも新しいスレッドを作成できます。 スレッドには、作業をすばやく完了して終了するものもあれば、プログラムの実行中アクティブ状態を続けるものもあります。

CRT ライブラリには、スレッドの作成と終了を行う関数 (_beginthread、_beginthreadex_endthread、および _endthreadex) が用意されています。

_beginthread 関数および _beginthreadex 関数は、新しいスレッドを作成し、スレッドが正常に作成されるとスレッド識別子を返します。 スレッドは、実行が完了すると自動的に終了します。 あるいは、_endthread または _endthreadex の呼び出しを使用して、自身を終了させることもできます。

Note

libcmt.lib を使用してビルドしたプログラムから C ランタイム ルーチンを呼び出す場合は、_beginthread 関数または _beginthreadex 関数でスレッドを開始する必要があります。 Win32 の ExitThread 関数および CreateThread 関数は使用しないでください。 また、C ランタイム ライブラリのデータ構造体へアクセス中のスレッドがあって、その完了を待っている複数のスレッドが存在する場合に SuspendThread を使うと、デッドロック状態になります。

_beginthread 関数と _beginthreadex 関数

_beginthread 関数および _beginthreadex 関数は、新しいスレッドを作成します。 スレッドは、プロセスのコードやデータ セグメントをプロセス内の他のスレッドと共有しますが、各スレッドには、独自のレジスタ値、スタック領域、および現在の命令アドレスがあります。 それぞれのスレッドに CPU 時間が与えられるので、プロセス中のすべてのスレッドを同時に実行できます。

_beginthread および _beginthreadex は、Win32 API の CreateThread 関数に似ていますが、次の違いがあります。

  • これらの関数は、特定の C ランタイム ライブラリ変数を初期化します。 これは、スレッドで C ランタイム ライブラリを使う場合に限り重要です。

  • CreateThread は、セキュリティ属性を制御します。 CreateThread を使うと、一時的に停止状態になっているスレッドの実行を再開できます。

_beginthread および _beginthreadex は、正常に終了した場合は新しいスレッドのハンドルを、エラーが発生した場合はエラー コードをそれぞれ返します。

_endthread 関数と _endthreadex 関数

_endthread 関数は、_beginthread によって作成されたスレッドを終了します (同様に、_endthreadex_beginthreadex によって作成されたスレッドを終了します)。 これらの関数が終了すると、スレッドは自動的に終了します。 _endthread および _endthreadex は、スレッドの内部条件に基づいて終了させる場合に便利です。 たとえば、通信ポートを制御できないときに、通信処理専用のスレッドを終了する場合があります。

マルチスレッド Win32 プログラムの作成

複数のスレッドを使用するプログラムを作成するときは、各スレッドの動作とプログラム リソースの使用を調節する必要があります。 また、スレッドごとに独自のスタックを確保するようにします。

スレッド間でのリソースの共有

Note

MFC の観点からの同様の説明については、マルチスレッドでのプログラミングのヒントに関する記事とマルチスレッドでの同期クラスの使用に関する記事を参照してください。

各スレッドは、専用のスタックと CPU レジスタの専用コピーを持っています。 ファイル、静的データ、ヒープ メモリなどのリソースは、プロセス内のすべてのスレッドで共有します。 これらの共通リソースを使うスレッドは、同期をとる必要があります。 Win32 には、セマフォや、クリティカル セクション、イベント、ミューテックスなど、リソースの同期をとるためにさまざまな方法が用意されています。

複数のスレッドが静的データにアクセスするときには、リソースの競合に備える必要があります。 たとえば、あるスレッドが表示する項目の x,y 座標が格納されている静的データ構造体を別のスレッドで更新するプログラムを考えてみます。 更新スレッドが x 座標を変更した後 y 座標を変える前に割り込まれてしまった場合、y 座標が更新される前に表示スレッドがスケジュールされることがあります。 その結果、項目は間違った場所に表示されます。 このような問題は、セマフォを使って構造体へのアクセスを制御することにより解決できます。

ミューテックス (mutex は mutual exclusion = 相互排他の先頭の文字を組み合わせた造語) は、お互いに非同期で実行しているスレッドやプロセス間で通信する方法の 1 つです。 この通信を使用して、共有リソースをロックおよびロック解除することにより共有リソースへのアクセスを制御することで、複数のスレッドやプロセスの処理を調整できます。 この x,y 座標の更新問題を解決するために、更新スレッドによって、更新を実行する前にデータ構造体が使用中であることを示すミューテックスを設定します。 両方の座標が更新されたら、ミューテックスを解除します。 表示スレッドは、ミューテックスが解除されるのを待ってから画面を更新する必要があります。 この場合プロセスはブロックされて、ミューテックスが解除されるまで先へ進むことができないので、ミューテックスを待つこのプロセスは、ミューテックスにより "ブロックされている" ということがあります。

サンプル マルチスレッド C プログラムに示されている Bounce.c プログラムは、ScreenMutex という名前のミューテックスを使用して画面の更新を調整します。 表示スレッドの 1 つで画面に書き込む準備が整うたびに、ScreenMutex へのハンドルと定数 INFINITE を指定して WaitForSingleObject を呼び出し、WaitForSingleObject の呼び出しによってミューテックスでブロックして、タイムアウトしないことを示します。ScreenMutex が解除されると、wait 関数は他のスレッドによって表示が妨げられないようにミューテックスを設定し、スレッドの実行を継続します。 ScreenMutex が解除されない場合、スレッドはミューテックスが解除されるまでブロックされます。 スレッドが表示の更新を完了すると、ReleaseMutex を呼び出してミューテックスを解除します。

画面表示と静的データは、注意深く管理する必要がある 2 つのリソース例にすぎません。 たとえば、プログラムには、同じファイルにアクセスする複数のスレッドが存在する場合があります。 別のスレッドがファイル ポインターを移動した可能性があるので、それぞれのスレッドは、読み書きする前にファイル ポインターをリセットする必要があります。 さらに、各スレッドは、ポインターを配置してからファイルにアクセスするまでの間に割り込みが発生しないようにする必要があります。 これらのスレッドは、WaitForSingleObject 呼び出しと ReleaseMutex 呼び出しでそれぞれのファイル アクセスを囲むことにより、セマフォを使ってファイルへのアクセスを調整する必要があります。 この手法を説明するコード例を次に示します。

HANDLE    hIOMutex = CreateMutex (NULL, FALSE, NULL);

WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

スレッド スタック

アプリケーションの既定のスタック領域はすべて、スレッド 1 と呼ばれる、最初に実行されるスレッドに必ず割り当てられます。 したがって、2 番目以降のスレッドのスタックに割り当てるメモリの量を指定する必要があります。 オペレーティング システムは、必要に応じて、スレッドに対してスタック領域をさらに割り当てますが、既定値は指定する必要があります。

_beginthread 呼び出しの最初の引数は、スレッドを実行する BounceProc 関数へのポインターです。 2 番目の引数では、スレッドに対する既定のスタック サイズを指定します。 最後の引数は、BounceProc へ渡される ID 番号です。 BounceProc は、ID 番号を使って乱数ジェネレーターをシードし、スレッドのパレット番号と表示文字を選択します。

C のランタイム ライブラリまたは Win32 API を呼び出すスレッドは、呼び出すライブラリと API 関数のために十分なスタック領域を用意する必要があります。 C 言語ライブラリの printf 関数には、500 バイト以上のスタック領域が必要です。また Win32 API ルーチンを呼び出すには、スタック領域として 2K バイト用意する必要があります。

スレッドはそれぞれ自分自身のスタックを持っているので、できる限り静的データを使わないことで、データ項目に対して起こりうる衝突を避けることができます。 スレッドが持つ各データに対しては、すべて auto 自動スタック変数を使うようにプログラムをデザインします。 Bounce.c プログラムの唯一のグローバル変数は、ミューテックスか、初期化後に変化しない変数のいずれかです。

Win32 には、スレッドの個別データを格納するために、スレッド ローカル ストレージ (TLS: Thread-Local Storage) も用意されています。 詳細については、「スレッド ローカル ストレージ (TLS)」を参照してください。

マルチスレッド プログラムの問題を回避する方法

マルチスレッドの C プログラムを作成、リンク、または実行する際に発生する可能性のある問題がいくつかあります。 次の表では、一般的な問題のいくつかについて説明します (MFC の観点からの同様の説明については、マルチスレッドでのプログラミングのヒントに関するページを参照してください)。

問題 考えられる原因
プログラムによって保護違反が発生したことを示すメッセージ ボックスが表示される。 さまざまな Win32 プログラミング エラーが原因で、保護違反が発生します。 一般的に、保護違反は、データを間接的に null ポインターに割り当てることによって発生します。 この結果、プログラムは、自身に属していないメモリにアクセスしようとするため、保護違反が発行されます。

保護違反の原因を検出する簡単な方法は、デバッグ情報を使用してプログラムをコンパイルし、Visual Studio 環境でデバッガーを使用して実行することです。 保護エラーが発生すると、Windows は制御をデバッガーに転送し、問題の原因となった行にカーソルが配置されます。
プログラムによって多数のコンパイル エラーとリンク エラーが生成される。 多くの潜在的な問題を取り除くには、コンパイラの警告レベルを最高値のいずれかに設定し、警告メッセージに従います。 レベル 3 やレベル 4 の警告レベル オプションを使用すると、意図しないデータ変換、欠落している関数プロトタイプ、非 ANSI 機能の使用を検出できます。

関連項目

旧形式のコードのためのマルチスレッド サポート (Visual C++)
C でのマルチスレッド プログラムのサンプル
スレッド ローカル ストレージ (TLS)
C++/WinRT を使用した同時開催操作と非同期操作
C++ と MFC を使用するマルチスレッド