マルチスレッド
マルチスレッドとは
今まで作成してきたプログラムは、処理を一つずつ順番に実行していきます。
前の処理が終わらない限り、次の処理に移ることはできません。
この処理の単位をスレッドと言います。
スレッドをひとつだけ持つプログラムをシングルスレッドプログラムといいます。
これに対して、処理の単位を複数持つプログラムはマルチスレッドプログラムと言います。
つまり同時に複数の処理を実行できるプログラムです。
複数の処理を同時実行することを並列処理といいます。
並列処理の目的はプログラムの高速化です。
プログラムを速くするといってもいくつかの意味がありますが、まず第一に「応答性の向上」が挙げられます。
プログラムでは、マウスやキーボードなどによるユーザー入力も処理のひとつです。
通常は入力すればすぐに結果が画面に反映されます。
(文字の入力やボタンのクリック、ウィンドウの移動など)
しかし、プログラムが何らかの重い処理(時間のかかる処理)を行っている間はユーザー入力への対応は処理が終わるまで後回しになってしまい、クリック等の操作をしても画面への反映が大きく遅れることがあります。
そのような場合、ユーザー入力に対応するスレッドと、重い処理をするスレッドとを分けることで、ユーザー入力の応答性を下げることなく重い処理を実行することができます。
上記の例はプログラム全体の処理時間は変わりませんが、プログラム全体の高速化を図ることもできます。
「重い処理」といっても、ディスクアクセスやネットワーク通信などCPUパワーをさほど使用しないものもあります。
シングルスレッドプログラムではこのような処理でも終わるまで待たなければ次の処理に移ることができませんが、マルチスレッドプログラムではCPUに別の処理をさせることで効率的な処理ができます。
並列処理と並行処理
並列処理に似たものに「並行処理」があります。
どちらも「同時に処理を行う」という意味では同じですが、並行処理はひとつのスレッドで複数の処理を「見かけ上は同時に」行います。
これは例えるなら「一人で複数の仕事を同時に行う」もので、仕事Aの処理中は仕事Bは待機状態に、仕事Bの処理に移ると仕事Aは待機状態になります。
ふたつ(以上)の処理を同時に進めますが、本当の意味での「同時」ではありません。
システムはこの処理の切り替えを高速に何度も行うため、見かけ上は同時に処理されているように見えるわけです。
並列処理は、仕事をする人数自体を増やして同じ時間中に複数の仕事を同時に処理します。
ただしマルチスレッドプログラムであっても、並列処理されるか否かはCPUによります。
2000年代初頭くらいまでのCPUは「シングルコア/シングルスレッド」というタイプのもので、これは同時演算数がひとつしかありません。
このCPUはいわば「全ての仕事(演算)を一人で処理する」タイプのもので、マルチスレッドプログラムを実行した場合でも並列処理はできずに並行処理になります。
つまり「スレッド1」がCPUに計算をさせている間、「スレッド2」はCPUが空くまで待機状態になります。
このCPUではシングルスレッドプログラムをマルチスレッド化しても全体の処理時間の短縮にはなりませんが、ユーザーの入力を処理するスレッドと重い処理を行うスレッドとを分けることで、応答性を向上させる効果が期待できます。
近年のCPUは「マルチコア/マルチスレッド」のものが大半で、2つ以上の演算を同時に処理することができます。
スレッド1の演算中にスレッド2の演算も同時に行うことができるため、プログラム全体の処理時間の短縮が期待できます。
WindowsなどのOSは複数のプログラムを同時に実行することができます。
これはマルチタスク(マルチプロセス)と言います。
マルチスレッドは、ひとつのプログラム中でマルチタスクを実現する方法と言えます。
(プログラムの実行単位はプロセスと言います)
スレッドの作成
CreateThread関数
プログラムはメインとなるスレッドをひとつ持っています。
マルチスレッドを実現するには新しいスレッドを作成する必要があります。
新しいスレッドはCreateThread
関数で作成できます。
- HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
); - 新しいスレッドを作成し、そのハンドルを返す。
失敗した場合はNULLを返す。
lpThreadAttributes
はSECURITY_ATTRIBUTES
構造体のポインタで、これはセキュリティ記述子というものです。
NULL
を指定することでデフォルトの設定が適用されます。
dwStackSize
はスレッドのスタックの初期サイズです。
(バイト単位。きっちり指定通りのサイズになるとは限らず、システムがメモリを管理する最小単位の倍数に調整されます)
スタックとはローカル変数や関数の引数などに使用されるメモリ領域のことです。
0
を指定すると初期値(メインスレッドと同じサイズ、通常は1MB)が適用されます。
lpStartAddress
はスレッドの開始位置となる関数(のアドレス)の指定です。
あらかじめ決められた形式の関数を使用します。
(詳しくは後述)
lpParameter
はスレッドに渡す追加の情報(ポインタ)です。
使用しない場合はNULL
を指定します。
dwCreationFlags
はスレッドの作成を制御するフラグで、以下の定数をしていします。
定数 | 説明 |
---|---|
0 | スレッドを作成後、すぐに実行する |
CREATE_SUSPENDED | スレッドを作成後、中断状態にする |
STACK_SIZE_PARAM_IS_A_RESERVATION | 引数dwStackSize はスタックの初期サイズではなく予約サイズである(初期サイズは規定値が使用される) |
lpThreadId
は作成されるスレッドの識別子(ID)のポインタです。
これは関数からスレッドを操作する時に使用されることがあります。
戻り値はスレッドのハンドルです。
(スレッド識別子とは異なります)
関数が失敗した場合はNULL
です。
スレッド開始関数
引数lpParameter
には、スレッド作成後に実行する関数を指定します。
この関数は以下の形式で定義します。
(関数名や引数名は自由です)
- DWORD WINAPI ThreadProc(
lpParameter
); - 新しいスレッドの作成後に実行される関数の定義。
引数lpParameter
は、CreateThread
関数の引数lpParameter
が格納されます。
戻り値にはスレッドの終了情報をDWORD型で指定することができます。
戻り値を指定しない場合は0が指定されたものとみなされます。
「259」という値は内部的に使用されているため指定してはいけません。
(STILL_ACTIVE
という定数で定義されている)
この関数(のアドレス)をCreateThread関数等の引数に指定するとき、LPTHREAD_START_ROUTINE
型にキャストしたほうが良いそうです。
サンプルコード
#include <windows.h>
#include <strsafe.h>
//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
DWORD WINAPI ThreadProc(LPVOID);
//ウィンドウの生成等は省略
#define BUFFERSIZE 11
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
WCHAR buf[BUFFERSIZE];
unsigned int count = 0;
HWND hWnd = (HWND)lpParameter;
HDC hdc = GetDC(hWnd);
while (1)
{
InvalidateRect((HWND)lpParameter, NULL, TRUE);
StringCchPrintf(buf, BUFFERSIZE, L"%u", count++);
TextOut(hdc, 10, 10, buf, lstrlen(buf));
}
ReleaseDC(hWnd, hdc);
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
DWORD dwThreadID;
switch (message)
{
case WM_CREATE: //ウィンドウの作成
//新しいスレッドの作成
CreateThread(
NULL, 0,
//スレッド作成後にこの関数が実行される
(LPTHREAD_START_ROUTINE)ThreadProc,
hWnd, //追加のパラメータにウィンドウハンドルを渡す
0, &dwThreadID);
break;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
実行するとウィンドウ内に数字が高速でカウントされていきます。
新しいスレッドで実行されるThreadProc関数には、追加の情報としてメインウィンドウのハンドルを渡しています。
関数内ではこのウィンドウハンドルを使用してデバイスコンテキストを取得し、ウィンドウ内に描画を行っています。
注目すべきは関数内に無限ループが存在する点です。
この無限ループは脱出する方法を用意しておらず、メインスレッドでこのような処理を行うとプログラムはフリーズしてしまいます。
しかしこのコードではウィンドウの操作等はメインスレッドが担当し、クライアント領域の描画(無限ループ)を別スレッドが担当しているため、フリーズせずに実行することができます。
今回のコードは作成したスレッドを終了させる処理がありませんが、プログラムの終了時にスレッドも終了します。
ファイルへのアクセス中などに終了させるとファイルが破損する危険があるため、本来は適切にスレッドを終了させる処理をすべきです。
スレッドの終了
ExitThread関数
新スレッドはThreadProc関数の終端に達すると終了します。
その他、ExitThread
関数を実行することでも終了できます。
- void ExitThread(
DWORD dwExitCode
); - この関数を実行したスレッドを終了コードdwExitCodeで終了させる。
dwExitCode
はスレッドの終了コードです。
これはreturn
文で戻り値を指定するのと同じです。
ただしC++ではこの関数よりもreturn文でスレッドを終了させる方が良いそうなので、実際にこの関数を使用することはあまりありません。
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
//どちらでも同じ
if (0) {
ExitThread(0);
}
else {
return 0;
}
}
TerminateThread関数
スレッドを強制終了させるにはTerminateThread
関数を使用します。
- BOOL TerminateThread(
HANDLE hThread,
DWORD dwExitCode
); - スレッドhThreadを終了コードdwExitCodeで終了させる。
この関数は終了させたいスレッド以外のスレッドからハンドルを指定して実行します。
スレッドの強制終了は推奨されません。
Windows XP、Windows Server 2003以前のOSでは、スレッド作成時に確保したスタック(メモリ領域)は開放されません。
それ以降のOSであっても、スレッド内の処理により確保されたメモリやリソースの解放処理は行われません。
それにより不具合が生じる可能性があります。
この関数によるスレッドの終了は最終手段です。
GetExitCodeThread関数
GetExitCodeThread
関数は、スレッドが終了しているか否かを取得します。
- BOOL GetExitCodeThread(
HANDLE hThread,
LPDWORD lpExitCode
); - スレッドhThreadの終了状態を取得する。
成功した場合は0以外を、失敗した場合は0を返す。
hThread
はスレッドのハンドルです。
lpExitCode
はスレッドの終了状態を受け取るDWORD型のポインタを指定します。
スレッドが実行中の場合はlpExitCodeにはSTILL_ACTIVE
という定数が格納されます。
スレッドが終了している場合はスレッド関数で指定した戻り値(return文またはスレッド終了関数で指定した値)が格納されます。
定数STILL_ACTIVEは内部的に「259」と定義されています。
スレッドの終了状態が正しく判定できなくなるため、この値を戻り値に指定してはいけません。
CloseHandle関数
スレッドが終了した後でもスレッドハンドルは有効です。
スレッドハンドルはスレッドオブジェクトという、スレッドから情報を取得したり操作したりするためのオブジェクトのハンドルです。
スレッドオブジェクトはスレッド自体が所有するメモリ領域とはまた別のメモリ領域に保存されていて、スレッドが終了しても勝手に削除されません。
(スレッドの終了状態をハンドルを通して取得できるのはそのためです)
スレッドオブジェクトを削除するにはCloseHandle関数でスレッドハンドルを閉じます。
ハンドルを閉じないとスレッドオブジェクトがメモリに残り続けるため、不要になったら必ず削除しましょう。
(すぐにプログラム自体を終了させるならば同時に解放されるので、明示的に削除しなくても問題はありません)
なお、終了する前のスレッドのハンドルをCloseHandle関数で閉じてもスレッドは終了しません。
あくまでもスレッド操作のためのオブジェクトが削除されるだけなので、スレッド自体はそれとは関係なく動作します。
スレッドオブジェクトが不要である場合はスレッド作成後にすぐに削除しても問題はなく、そのほうが削除し忘れの心配もありません。
スレッドを終了させる関数に_endthread
関数というものがあり(_endthreadex
関数とは別物)、これはスレッドを終了させた後にスレッドハンドルも閉じます。
そのため、この関数を使用する場合はCloseHandle関数でスレッドハンドルを閉じてはいけません。
サンプルコード
#include <windows.h>
#include <strsafe.h>
//ウィンドウの生成等は省略
#define BUFFERSIZE 11
typedef struct {
HWND hWnd;
unsigned int count;
} MYSTRUCT;
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
const unsigned int COUNTMAX = 10000;
MYSTRUCT* ms = (MYSTRUCT*)lpParameter;
WCHAR buf[BUFFERSIZE];
unsigned int count = 0;
HDC hdc = GetDC(ms->hWnd);
while (count++ < COUNTMAX)
{
InvalidateRect((HWND)lpParameter, NULL, TRUE);
StringCchPrintf(buf, BUFFERSIZE, L"%u", ++ms->count);
TextOut(hdc, 10, 10, buf, lstrlen(buf));
}
ReleaseDC(ms->hWnd, hdc);
return 0;
}
#define IDC_BUTTON1 100
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HANDLE hThread;
static MYSTRUCT ms;
DWORD dwExitCode;
switch (message)
{
case WM_CREATE: //ウィンドウの作成
ms.hWnd = hWnd;
break;
case WM_LBUTTONUP: //マウス左ボタンアップ
//終了コードの確認
if (GetExitCodeThread(hThread, &dwExitCode))
{
//アクティブなら何もしない
if (dwExitCode == STILL_ACTIVE)
break;
//ハンドルを閉じる
CloseHandle(hThread);
}
//新しいスレッドの作成
hThread = CreateThread(
NULL, 0,
(LPTHREAD_START_ROUTINE)ThreadProc,
&ms, 0, NULL);
break;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
実行結果です。
クライアント領域をクリックする度に10000ずつ数値が増加していきます。
ThreadProc関数にはLPVOID型(void*型)の引数ひとつしかパラメータを渡せないので、必要なデータを構造体で定義してそのポインタを渡しています。
WM_LBUTTONUP
メッセージでは、GetExitCodeThread
関数を使用してスレッドの実行状態を取得し、実行中の場合は何もせずに終了します。
スレッドが終了している場合はスレッドを破棄し、新しいスレッドを作成します。
つまり、同時に作成されるスレッドはひとつまでに制限しています。
(メインスレッドを入れればふたつ)
ちなみに一度もスレッドを作成していない場合(ハンドルがNULL
の場合)はGetExitCodeThread関数は失敗します。
_beginthreadex関数、_endthreadex関数
CreateThread
関数で作成したスレッドでは、いくつかのC言語標準関数を使用すると問題が発生する可能性があります。
C言語標準関数を使用する場合は_beginthreadex
関数でスレッドを作成する必要があります。
(先頭にアンダーバーがあるので注意)
引数はCreateThread関数と同じですが、戻り値はuintptr_t型(unsigned int型の別名)なので、変数に格納する場合はHANDLE型にキャストする必要があります。
_beginthreadex関数で作成したスレッドを閉じるには_endthreadex
関数を使用します。
引数はExitThread
関数と同じです。
これらの関数の使用にはprocess.h
のインクルードが必要です。
CreateThread関数で作成したスレッドを_endthreadex関数で終了することや、その逆はできないので注意してください。
#include <windows.h>
#include <process.h>
HANDLE hThread;
//hThread = CreateThread(
hThread = (HANDLE)_beginthreadex(
NULL, 0,
(LPTHREAD_START_ROUTINE)ThreadProc,
0, 0, NULL);
当サイトではこれ以降、スレッドの作成には_beginthreadex
関数、_endthreadex
関数を使用して解説します。