ミューテックス
プロセス間の排他制御
ミューテックスは、各プロセスから共通してアクセス可能なミューテックスオブジェクトをシステムに作成することでアクセス制御を行う手法です。
これはクリティカルセクションオブジェクトに似たもので、スレッドだけでなくプロセス間でも排他制御が可能です。
ミューテックスの作成
CreateMutex関数
ミューテックスオブジェクトはCreateMutex関数で作成します。
- HANDLE CreateMutexW(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCWSTR lpName
); - ミューテックスオブジェクトを作成する。
または既存のミューテックスオブジェクトを開く。
- lpMutexAttributes
-
ミューテックスオブジェクトのセキュリティ記述子です。
NULLを指定するとハンドルの子プロセスへの継承は禁止されます。
通常はNULLで構いません。 - bInitialOwner
-
TRUEを指定すると、この関数の呼び出し元プロセスがミューテックスを作成した場合、直ちにミューテックスオブジェクトの所有権を取得します。
FALSEを指定すると所有権を取得しません。 - lpName
-
ミューテックスオブジェクトの名前を指定します。
この名前のミューテックスオブジェクトがシステムに存在しない場合は作成され、システムに登録されます。
すでに存在する場合はMUTEX_ALL_ACCESSというアクセス権(所有権)を要求します。
この場合、引数bInitialOwnerは無視されます。
引数lpMutexAttributesのbInheritHandleメンバにより、ハンドルの子プロセスへの継承を設定することは可能ですが、セキュリティ記述子は無視されます。名前は大文字小文字を区別します。
長さは最大でMAX_PATHまで指定可能です。
既存のセマフォ、イベント、待機可能タイマー、ジョブ、ファイルマッピングオブジェクトの名前と重複すると関数は失敗します。
NULLを指定すると名前なしミューテックスを作成します。名前なしミューテックスは他のプロセスからは使用できません。
同じプロセスの別スレッドからは使用できるので、クリティカルセクションオブジェクトと同じような使い方ができます。 - 戻り値
-
関数が成功した場合はミューテックスオブジェクトのハンドルを返します。
指定した名前のミューテックスオブジェクトが既に存在する場合も関数は成功しますが、GetLastError関数はERROR_ALREADY_EXISTSを返します。
関数が失敗した場合はNULLを返します。
ミューテックスオブジェクトは、不要になったらCloseHandle関数でハンドルを閉じる必要があります。
ミューテックスオブジェクトは複数のスレッド/プロセスから参照することができ、CloseHandle関数によってひとつも参照するスレッド/プロセスがなくなった場合にシステムから削除されます。
(明示的にCloseHandle関数を呼ばなくてもアプリケーションが終了した時にハンドルは解放されます)
サンプルコード(二重起動の防止)
ミューテックスを使用してアプリケーションの二重起動を防止するサンプルです。
#include <windows.h>
HINSTANCE hInst; //インスタンス
WCHAR szTitle[] = L"テストアプリ"; //タイトル
WCHAR szWindowClass[] = L"MyTestApp"; //ウィンドウクラス名
WCHAR szMutexName[] = L"programming.pc-note.net:MyTestApp_Mutex"; //ミューテックス名
//ウィンドウの生成等は省略
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HANDLE hMutex;
switch (message)
{
case WM_CREATE: //ウィンドウの作成
//ミューテックスの作成
hMutex = CreateMutex(NULL, FALSE, szMutexName);
//すでに同名のミューテックスが存在する
if (GetLastError() == ERROR_ALREADY_EXISTS) {
if (hMutex)
CloseHandle(hMutex);
MessageBox(hWnd, L"二重起動はできません。", L"エラー", MB_OK);
return -1;
}
break;
case WM_DESTROY: //ウィンドウの破棄
if (hMutex)
CloseHandle(hMutex);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
ビルドした実行ファイルを二重起動しようとすると、メッセージボックスが表示されアプリケーションが終了します。
今回は「システムにミューテックスオブジェクトが存在するか否か」だけで判定が可能なので、ミューテックスオブジェクトの所有権は取得していません。
ミューテックス名は他のアプリケーションと重複しないように気を付ける必要があります。
自分の名前や会社名などを入れると重複の可能性を下げることができます。
プロセス間通信等を行える場合、作成の度にランダムな文字列にした上で他のプロセスにミューテックス名を渡すことによって重複の可能性を下げることができます。
ミューテックスオブジェクトハンドルの取得
OpenMutex関数
システム上に存在するミューテックスオブジェクトのハンドルを取得するにはOpenMutex関数を使用します。
- HANDLE OpenMutexW(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCWSTR lpName
); - 既存のミューテックスオブジェクトlpNameのハンドルを取得する。
失敗した場合はNULLを返す。
- dwDesiredAccess
-
ミューテックスオブジェクトに要求するアクセス権の指定です。
通常のミューテックスの使い方をする場合はSYNCHRONIZEを指定します。
MUTEX_ALL_ACCESSはミューテックスオブジェクトのセキュリティを変更する場合に指定します。
呼び出し元プロセスにアクセス権がない場合は関数は失敗します。
Win9x系ではSYNCHRONIZEは使用できないのでMUTEX_ALL_ACCESSを指定します。 - bInheritHandle
-
取得したハンドルを子プロセスに継承できるか否かを
TRUE/FALSEで指定します。 - lpName
-
開くミューテックス名を指定します。
名前なしミューテックスは取得できません。 - 戻り値
-
成功した場合はミューテックスオブジェクトのハンドルです。
失敗した場合はNULLを返し、GetLastError関数はERROR_FILE_NOT_FOUNDを返します。
この関数はミューテックスオブジェクトのハンドルを取得するだけで、所有権を要求する機能はありません。
所有権の取得と解放
WaitForSingleObject関数
既存のミューテックスオブジェクトの所有権を得る方法はいくつかあり、待機関数というものを使用します。
最もシンプルなのはWaitForSingleObject関数です。
- DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
); - オブジェクトhHandleがシグナル状態になるまでdwMillisecondsミリ秒待機する。
ミューテックスオブジェクトなどは、オブジェクトが使用可能状態になるまで待機する関数が提供されています。
オブジェクトが使用可能な状態をシグナル状態といい、他のスレッドから使用されていて他からは使用できない状態をノンシグナル状態(非シグナル状態)といいます。
WaitForSingleObject関数は、ハンドルhHandleが指すオブジェクトがシグナル状態になるまで待機する関数です。
第二引数dwMillisecondsは待機する最大の時間(タイムアウト時間)をミリ秒で指定します。
0を指定すると関数はすぐに制御を返します。
定数INFINITEを指定すると、シグナル状態になるまで(所有権を得られるまで)タイムアウトすることなく待ち続けます。
戻り値は以下の定数のいずれかです。
| 定数 | 説明 |
|---|---|
| WAIT_OBJECT_0 | オブジェクトはシグナル状態である (所有権を取得) |
| WAIT_ABANDONED | ミューテックスオブジェクトを所有するスレッドが所有権を解放する前に終了した オブジェクトの所有権は関数を呼び出したスレッドに移り、状態をノンシグナルに設定する (所有権を取得) ミューテックスオブジェクト以外の場合はこの値は返さない。 |
| WAIT_TIMEOUT | タイムアウト時間の経過 (所有権は取得できない) |
| WAIT_FAILED | 関数の失敗 |
WaitForMultipleObjects関数
複数のオブジェクトのシグナル状態を待つ場合はWaitForMultipleObjectsを使用します。
- DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE *lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
); - nCount個のオブジェクト配列lpHandlesのひとつ以上がシグナル状態になるまでdwMillisecondsミリ秒待機する。
- nCount
-
オブジェクトの個数を指定します。
0は指定できません。
最大数は定数MAXIMUM_WAIT_OBJECTSです。 - lpHandles
-
オブジェクトの配列を指定します。
- bWaitAll
-
TRUEを指定すると、配列中のオブジェクトのすべてがシグナル状態になった時に制御を返します。
FALSEを指定すると、配列中のオブジェクトのひとつ以上がシグナル状態になった時に制御を返します。 - dwMilliseconds
-
待機するタイムアウト時間をミリ秒で指定します。
- 戻り値
-
待機するタイムアウト時間をミリ秒で指定します。
定数 説明 WAIT_OBJECT_0
~
(WAIT_OBJECT_0 + nCount - 1)bWaitAllがTRUEの場合、この範囲の値は全てのオブジェクトがシグナル状態であることを意味する。bWaitAllがFALSEの場合、戻り値からWAIT_OBJECT_0をマイナスした値はシグナル状態になったオブジェクトの配列のインデックスを意味する。
複数のオブジェクトがシグナル状態である場合、その中の最小のインデックス値となる。WAIT_ABANDONED
~
(WAIT_ABANDONED + nCount - 1)bWaitAllがTRUEの場合、この範囲の値は全てのオブジェクトがシグナル状態であり、少なくともひとつのオブジェクトが放棄されたオブジェクト(オブジェクトが解放される前にスレッドが終了したオブジェクト)であることを意味する。bWaitAllがFALSEの場合、戻り値からWAIT_ABANDONEDをマイナスした値は放棄されたオブジェクトの配列のインデックスを意味する。WAIT_TIMEOUT タイムアウト時間の経過 WAIT_FAILED 関数の失敗
ReleaseMutex関数
ミューテックスオブジェクトの所有権を解放するにはReleaseMutex関数を使用します。
- BOOL ReleaseMutex(
HANDLE hMutex
); - ミューテックスオブジェクトハンドルhMutexの所有権を解放する。
成功した場合は0以外を、失敗した場合は0を返す。
これは特に難しくないでしょう。
呼び出し元スレッドがオブジェクトの所有権を持っていない場合は関数は失敗します。
サンプルコード
#include <windows.h>
#include <process.h>
//ウィンドウの生成等は省略
#define WM_LOCKMUTEX (WM_APP + 1)
#define WM_UNLOCKMUTEX (WM_APP + 2)
WCHAR szMutexName[] = L"programming.pc-note.net:MyTestApp_Mutex"; //ミューテックス名
//スレッド開始関数
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
//既存のミューテックスオブジェクトハンドルを開く
//(所有権の要求ではない)
HANDLE hMutex = OpenMutex(SYNCHRONIZE, FALSE, szMutexName);
if (!hMutex)
return 0;
//所有権を得られるまで待機
WaitForSingleObject(hMutex, INFINITE);
//適当に何か処理
PostMessage((HWND)lpParameter, WM_LOCKMUTEX, 0, 0);
Sleep(1000);
PostMessage((HWND)lpParameter, WM_UNLOCKMUTEX, 0, 0);
//ミューテックスの所有権を解放
ReleaseMutex(hMutex);
CloseHandle(hMutex);
return 0;
}
#define BUFFERSIZE 32
#define IDC_BUTTON1 100
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static unsigned int waitingCount; //ミューテックス解放待ちスレッドの数
static HWND hStatic;
static HANDLE hMutex;
HANDLE hThread;
WCHAR buf[BUFFERSIZE];
WCHAR* format[] = {
L"ミューテックス所有スレッドなし",
L"オブジェクト解放待ち: %u"
};
switch (message)
{
case WM_CREATE: //ウィンドウの作成
//ミューテックスの作成
hMutex = CreateMutex(NULL, FALSE, szMutexName);
if (!hMutex) {
MessageBox(hWnd, L"ミューテックスの作成に失敗。", L"エラー", MB_OK);
DestroyWindow(hWnd);
break;
}
CreateWindow(L"BUTTON", L"新スレッド開始",
WS_CHILD | WS_VISIBLE,
10, 10,
120, 25,
hWnd, (HMENU)IDC_BUTTON1, hInst, NULL);
hStatic = CreateWindow(L"STATIC", format[0],
WS_CHILD | WS_VISIBLE,
10, 40,
250, 25,
hWnd, (HMENU)-1, hInst, NULL);
break;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BUTTON1:
hThread = (HANDLE)_beginthreadex(
NULL, 0,
(LPTHREAD_START_ROUTINE)ThreadProc,
hWnd, 0, NULL);
if (hThread != NULL) {
CloseHandle(hThread);
waitingCount++;
StringCchPrintf(buf, BUFFERSIZE, format[1], waitingCount);
SetWindowText(hStatic, buf);
}
break;
}
break;
case WM_LOCKMUTEX: //ミューテックス所有
waitingCount--;
StringCchPrintf(buf, BUFFERSIZE, format[1], waitingCount);
SetWindowText(hStatic, buf);
break;
case WM_UNLOCKMUTEX: //ミューテックス解放
if(waitingCount > 0)
StringCchPrintf(buf, BUFFERSIZE, format[1], waitingCount);
else
StringCchPrintf(buf, BUFFERSIZE, format[0]);
SetWindowText(hStatic, buf);
break;
case WM_DESTROY: //ウィンドウの破棄
if (hMutex)
CloseHandle(hMutex);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
ボタンクリックで新スレッドを作成し、メインスレッドで作成されたミューテックスオブジェクトを所有するサンプルコードです。
今回はOpenMutex関数を使用していますが、同じプロセス内での処理なので実際のコードでは名前なしミューテックスを作成してオブジェクトハンドルをスレッド間で共有したほうが良いでしょう。
名前ありミューテックスは二重起動したプロセスとも共有してしまうため、そのままでは不都合である場合があります。
サンプルコード2
メインスレッドではミューテックスオブジェクトを持たず、新しく作成されるスレッド側だけで管理するサンプルです。
#include <windows.h>
#include <strsafe.h>
#include <process.h>
//ウィンドウの生成等は省略
#define WM_LOCKMUTEX (WM_APP + 1)
#define WM_UNLOCKMUTEX (WM_APP + 2)
//スレッド引数用の構造体
typedef struct {
HWND hWnd;
WCHAR* mutexName;
} ThreadParam;
//スレッド開始関数
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
ThreadParam *tp = (ThreadParam*)lpParameter;
//ミューテックスを作成
//すでに同名のミューテックスが存在する場合は開く
HANDLE hMutex = CreateMutex(NULL, TRUE, tp->mutexName);
if (!hMutex)
return 0;
//所有権を得られるまで待機
WaitForSingleObject(hMutex, INFINITE);
//適当に何か処理
PostMessage(tp->hWnd, WM_LOCKMUTEX, 0, 0);
Sleep(1000);
PostMessage(tp->hWnd, WM_UNLOCKMUTEX, 0, 0);
//ミューテックスの所有権を放棄
ReleaseMutex(hMutex);
CloseHandle(hMutex);
//この時点でミューテックスオブジェクトを参照するスレッドが
//存在しなくなった場合はミューテックスが破棄される
return 0;
}
#define BUFFERSIZE 32
#define IDC_BUTTON1 100
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static unsigned int waitingCount;
static HWND hStatic;
HANDLE hThread;
WCHAR buf[BUFFERSIZE];
WCHAR* format[] = {
L"ミューテックス所有スレッドなし",
L"オブジェクト解放待ち: %u"
};
//ミューテックス名
static WCHAR szMutexName[64];
//スレッド引数用の構造体
static ThreadParam tp;
//↑これらはプログラム実行中は常にメモリ上に存在しなければならないので
//static変数で宣言
switch (message)
{
case WM_CREATE: //ウィンドウの作成
//ランダムなミューテックス名を作成
StringCchPrintf(szMutexName, 64, L"Mutex_%u", GetTickCount());
tp = (ThreadParam){ hWnd, szMutexName };
CreateWindow(L"BUTTON", L"新スレッド開始",
WS_CHILD | WS_VISIBLE,
10, 10,
120, 25,
hWnd, (HMENU)IDC_BUTTON1, hInst, NULL);
hStatic = CreateWindow(L"STATIC", format[0],
WS_CHILD | WS_VISIBLE,
10, 40,
250, 25,
hWnd, (HMENU)-1, hInst, NULL);
break;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BUTTON1:
hThread = (HANDLE)_beginthreadex(
NULL, 0,
(LPTHREAD_START_ROUTINE)ThreadProc,
&tp,
0, NULL);
if (hThread != NULL) {
CloseHandle(hThread);
waitingCount++;
StringCchPrintf(buf, BUFFERSIZE, format[1], waitingCount);
SetWindowText(hStatic, buf);
}
break;
}
break;
case WM_LOCKMUTEX: //ミューテックス所有
waitingCount--;
StringCchPrintf(buf, BUFFERSIZE, format[1], waitingCount);
SetWindowText(hStatic, buf);
break;
case WM_UNLOCKMUTEX: //ミューテックス解放
if(waitingCount > 0)
StringCchPrintf(buf, BUFFERSIZE, format[1], waitingCount);
else
StringCchPrintf(buf, BUFFERSIZE, format[0]);
SetWindowText(hStatic, buf);
break;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
実行結果はひとつ上のサンプルコードと同じです。
CreateMutex関数は、指定した名前のミューテックスが存在しない場合は作成し、存在する場合はオブジェクトハンドルを返すので、ハンドルをグローバル変数やメインスレッド側で保存しておく必要はありません。
ミューテックス名は簡易的にランダムにするためにWindows起動からの時間(GetTickCount関数)から生成しているので、二重起動された場合でも独立して動作します。
(ただしこの方法はミリ秒まで同じタイミングで起動された場合は重複します)