二重起動の防止(3)

プロセス間の同期

前回紹介した共有メモリとウィンドウメッセージを利用する二重起動防止の手順により、2回目以降に起動されたプロセスからすでに起動されている側のプロセスに向けて通知を送ることができるようになりました。前回は共有メモリにウィンドウハンドルのみを格納していましたが、応用すればファイル名を渡したりすることもできるでしょう。

ところが、前回作成したプログラムは、実は完全なものではないのです。なぜかというと、前回作成されたプログラムでは、プロセス間で同期が取られていないという問題があるのです。このプログラムでは、いったいどういうことかというと、次のようなことです。

前回作成したプログラムでは、最初に共有メモリ(ファイルマッピングオブジェクト)を作成したプロセスが自分のウィンドウハンドルを共有メモリに格納し、2回目以降のプロセスが共有メモリからウィンドウハンドルを取得して、そのウィンドウに通知を送るということを行っていました。では、もし仮に、最初のプロセスが共有メモリを作成した後で、その共有メモリにウィンドウハンドルを格納するよりも前に、2回目に起動されたプロセスが共有メモリからウィンドウハンドルを読み込もうとしたらどうなるでしょう。2回目に起動されたプロセスは、まだウィンドウハンドルの格納されていない共有メモリから、ウィンドウハンドルではない無効な値を読み込んでしまうことでしょう。これでは、最初に起動されたプロセスのウィンドウにメッセージを送ることはできなくなってしまいます。

また、共有メモリで受け渡すデータがウィンドウハンドルのみの場合は、プロセス終了時までハンドル値が変わらないためさほど大きな問題にはならないのですが、ウィンドウハンドル以外のデータをやり取りするとなると、さらに問題が出てきます。それは、2つ以上のプロセスが同時に共有メモリにアクセスしてしまう可能性があるということです。これでは、せっかく送ろうとしたデータが壊れてしまう可能性がありますね。

このような問題を防ぐには、プロセス間で同期(synchronization)を取ることが必要になります。同期を取るとはすなわち、ある条件が整うまでそのプロセスを(正確にはスレッドを)眠らせてしまうということです。あるスレッドが共有メモリにアクセスしている間、その作業が終わるまでは、共有メモリにアクセスしようとした他のスレッドはその実行を停止させておきます。そして、作業が終わった時点で、停止されていたスレッドが動き出すようにするのです。

今回のように、複数のプロセスやスレッドで共有されるある1つのオブジェクト(共有メモリなど)に対して同時に1つのスレッドだけがアクセスできるように同期を取ることを特に排他制御あるいは排他アクセスと呼びます。

ミューテックスオブジェクトによる同期

プロセス間で排他制御を行う方法の1つに、ミューテック(ミューテックスオブジェクト)を使うというものがあります。以前にも述べたように、ミューテックスオブジェクトには『被所有者』としての機能があり、スレッドによって「所有されている」状態と「所有されていない」状態のいずれかの状態を持っています。そのときはこの『所有』の概念は使われませんでしたが、今回はこれを使うことになります。

ミューテックスオブジェクトについては『二重起動の防止(1)』を参照してください。

簡単に復習しておきましょう。ミューテックスオブジェクトがとりうる状態とは、以下の2つの状態のことでした。

次に、待機関数の1つであるWaitForSingleObject関数の機能は、引数として渡されるオブジェクトの状態によって処理が異なり、以下のような作用を持っていました。

そして、WaitForSingleObject関数のもう1つの機能として、以下の作用があるのです。

つまり、ミューテックスオブジェクトに対して待機関数WaitForSingleObjectを呼び出すという動作は、ミューテックスオブジェクトの所有権を要求するということなのです。

以上のことから、ミューテックスオブジェクトと待機関数を組み合わせたときの動作は、次のようになることが分かります。

ミューテックスオブジェクトに対して待機関数を呼び出したとき、どのスレッドもそのオブジェクトを所有していない(オブジェクトがシグナル状態である)場合は、呼び出し側スレッドはオブジェクトの所有権を得てそのまま処理が続行されます。このとき、オブジェクトは所有された状態に移るわけですから、オブジェクトはノンシグナル状態になります。

一方、待機関数が呼び出されたとき、他のスレッドがそのオブジェクトを所有している(オブジェクトがノンシグナル状態である)場合は、所有スレッドがオブジェクトの所有権を解放するまで(所有スレッドがオブジェクトを所有しなくなり、オブジェクトがシグナル状態になるまで)、呼び出し側スレッドの実行を停止させます。そして、オブジェクトがシグナル状態になったとき、呼び出し側スレッドは所有権を得ることができて処理を再開し、オブジェクトは再びノンシグナル状態になります。

ミューテックスオブジェクトによる共有メモリへの排他アクセス制御を行うには、「共有メモリにアクセスするスレッドはミューテックスオブジェクトの所有権を持っていなければならない」という前提条件を追加します。このようにすれば、共有メモリにアクセスできるスレッドを同時にただ1つだけに限定することができますね。この

ミューテックスオブジェクトの所有権 = 共有メモリにアクセスする権利

という考え方が、今回の同期を行なうための重要な考え方になります。

ミューテックスオブジェクトの所有権の解放

ミューテックスオブジェクトを所有しているスレッドが、そのオブジェクトを手放すための方法をこれまで説明していなかったので、ここで説明しておきましょう。ミューテックスオブジェクトの所有権を解放するにはReleaseMutex関数を呼び出します。

BOOL ReleaseMutex(
    HANDLE hMutex   // ミューテックスのハンドル
);

この関数は、呼び出しスレッドが指定されたミューテックスオブジェクトを所有している場合に、そのオブジェクトの所有権を解放します。この関数によってミューテックスオブジェクトが解放されると、そのオブジェクトはシグナル状態になります。もしも、待機関数によって同じオブジェクトがシグナル状態になるのを待って停止されている別のスレッドがあれば、そのスレッドに所有権が渡されて、そのスレッドの実行が再開されます。

実際の手順

では、今回の手順を説明しましょう。

  1. ミューテックスオブジェクトを作成する。ただしこのときには所有権を取得しない。
  2. 待機関数を実行し、ミューテックスオブジェクトの所有権を得る。このとき、別のスレッドがミューテックスを所有していれば、それが解放されるまで実行が停止される。
  3. 共有メモリとしてのファイルマッピングオブジェクトを作成する。
  4. ファイルマッピングオブジェクトがすでに作成されていたかどうかを調べる。

ミューテックスオブジェクト作成時には、所有権を取得した状態で作成するかどうかを指定することができます。もしここで所有権を取得するように指定すると、初回起動時にはミューテックスオブジェクトの所有権を持った状態で生成されますが、2回目以降の起動ですでに存在する名前のミューテックスオブジェクトを作成しようとした場合には、所有権を持たない状態で既存のハンドルが返されるため、後で所有権を取得するためにWaitForSingleObject関数を呼び出すことになります。初回時と2回目以降とで所有権の取得方法に違いがあると混乱する可能性もあるので、生成時に所有権を取得しない方法にしておくことにします。

上の手順でもう1つ注目すべきことは、プロセスが最初に起動されたものかどうかを判定するのに、同名のミューテックスオブジェクトがすでに存在しているかどうかではなく、同名のファイルマッピングオブジェクトがすでに存在しているかどうかで判断しています。なぜミューテックスオブジェクトの存在で判断しないのかというと、最初にミューテックスオブジェクトを作成したプロセスが必ずしも最初にその所有権を得るとは限らないからです。最初にミューテックスオブジェクトを作成したプロセスが待機関数を呼び出してミューテックスオブジェクトの所有権を得ようとするまでの間に、2番目にミューテックスオブジェクト作成関数を呼び出したプロセスがさらにそのまま待機関数を呼び出して先に所有権を得てしまうという可能性があります。これを防ぐために、ファイルマッピングオブジェクトを初回起動判定に使用するのです。

サンプルスクリプト

今回のサンプルは、実行させたときの動作が前回のものと同じになります。前回と同じく、処理を各部分に分けてみていくことにします。

起動時(共通)

まずは以下の処理をするためのスクリプトです。

  1. ミューテックスオブジェクトを作成する。ただしこのときには所有権を取得しない。
  2. 待機関数を実行し、ミューテックスオブジェクトの所有権を得る。このとき、別のスレッドがミューテックスを所有していれば、それが解放されるまで実行が停止される。
  3. 共有メモリとしてのファイルマッピングオブジェクトを作成する。
  4. ファイルマッピングオブジェクトがすでに作成されていたかどうかを調べる。
; ファイルマッピングオブジェクトの名前の定義
#define MAP_OBJ_NAME  "HSP_WinAPI_Test_Mapping_Object"

; ミューテックスオブジェクトの名前の定義
#define MUTEX_NAME    "HSP_WinAPI_Test_Mutex_Object"

; ミューテックスオブジェクトの作成
hMutex = CreateMutex(0, 0, MUTEX_NAME)

; 待機関数を実行 (排他制御)
WaitForSingleObject hMutex, INFINITE

; ファイルマッピングオブジェクトの作成
sharedMemSize = 1024
hMapObj = CreateFileMapping(-1,0,PAGE_READWRITE,0,sharedMemSize,MAP_OBJ_NAME)

; ファイルマッピングオブジェクトが作成されていたかどうかの判定
if (GetLastError() == ERROR_ALREADY_EXISTS) {
    ; すでに同じ名前のファイルマッピングオブジェクトが存在する
    goto *SecondRunProc
} else {
    ; ファイルマッピングオブジェクトが新しく作成された
    goto *FirstRunProc
}

ファイルマッピングオブジェクト未作成時(初回起動時)

ファイルマッピングオブジェクトが作成されていなかった場合(初回起動の場合)の処理です。

  1. 通知用のアプリケーション定義メッセージを取得するように設定する。
  2. 共有メモリにメインウィンドウのハンドルを格納する。
  3. ミューテックスオブジェクトの所有権を解放する。
  4. 通知用のアプリケーション定義メッセージが送られてくるのを待つ。
  5. 終了時にファイルマッピングオブジェクトとミューテックスオブジェクトをクローズする。
; プログラム起動通知用ウィンドウメッセージを定義
#define MYWM_RUN_NOTIFY  (WM_USER + 1)

*FirstRunProc
; プログラム終了時処理の登録
onexit gosub *OnAppQuit

; 割り込み処理メッセージの登録
oncmd gosub *OnRunNotify, MYWM_RUN_NOTIFY

; ビューのマッピング
sharedMemPtr = MapViewOfFile(hMapObj,FILE_MAP_WRITE,0,0,sharedMemSize)

; ウィンドウハンドルを共有メモリに格納
dupptr sharedMemVal, sharedMemPtr, sharedMemSize
sharedMemVal(0) = hwnd

; ビューのマッピング解除
UnmapViewOfFile sharedMemPtr

sharedMemPtr = 0
dim sharedMemVal, 1

; ミューテックスオブジェクトの所有権を解放
ReleaseMutex hMutex
stop

*OnRunNotify
; 二重起動通知メッセージを受け取ったときの処理
gsel 0, 2
dialog "起動されました"
gsel 0, 1
return

*OnAppQuit
; ファイルマッピングオブジェクトとミューテックスのクローズ(終了時)
CloseHandle hMapObj
CloseHandle hMutex
end

マッピングオブジェクト作成時(2回目以降起動)

マッピングオブジェクトが作成されていた場合(2回目以降の起動の場合)の処理です。

  1. 共有メモリからウィンドウハンドルを読み込む。
  2. ミューテックスオブジェクトの所有権を解放する。
  3. 通知用のアプリケーション定義メッセージを送信する。
  4. ファイルマッピングオブジェクトとミューテックスオブジェクトをクローズして終了する。
*SecondRunProc
; ビューのマッピング
sharedMemPtr = MapViewOfFile(hMapObj,FILE_MAP_READ,0,0,sharedMemSize)

; 共有メモリからウィンドウハンドル取得
dupptr sharedMemVal, sharedMemPtr, sharedMemSize
hwndTarget = sharedMemVal(0)

; ビューのマッピング解除
UnmapViewOfFile sharedMemPtr
sharedMemPtr = 0
dim sharedMemVal, 1

; ミューテックスオブジェクトの所有権を解放
ReleaseMutex hMutex

; メッセージをポスト
PostMessage hwndTarget, MYWM_RUN_NOTIFY, 0, 0

; ファイルマッピングオブジェクトとミューテックスのクローズ
CloseHandle hMapObj
CloseHandle hMutex
end

サンプルスクリプト(まとめ)

サンプルの全体のスクリプトです。前回のスクリプトにおいて、ファイルマッピングオブジェクトに対する操作の前後でミューテックスオブジェクトによる排他制御の処理が追加されていることが分かります。

; Windows API 関数定義
#uselib "kernel32.dll"
#func  CloseHandle  "CloseHandle"  sptr
#cfunc CreateFileMapping  "CreateFileMappingA" sptr,sptr,sptr,sptr,sptr,sptr
#cfunc CreateMutex  "CreateMutexA" sptr,sptr,sptr
#cfunc GetLastError  "GetLastError"
#cfunc MapViewOfFile  "MapViewOfFile" sptr,sptr,sptr,sptr,sptr
#func  ReleaseMutex  "ReleaseMutex" sptr
#func  UnmapViewOfFile  "UnmapViewOfFile" sptr
#func  WaitForSingleObject "WaitForSingleObject" sptr,sptr

#uselib "user32.dll"
#func PostMessage  "PostMessageA" sptr,sptr,sptr,sptr
#func SetForegroundWindow  "SetForegroundWindow" sptr

; Windows API 定数定義
#define FILE_MAP_WRITE        2
#define FILE_MAP_READ         4
#define PAGE_READWRITE        4
#define ERROR_ALREADY_EXISTS  183
#define WM_USER               0x0400

; プログラム起動通知用ウィンドウメッセージを定義
#define MYWM_RUN_NOTIFY  (WM_USER + 1)

; ファイルマッピングオブジェクトの名前の定義
#define MAP_OBJ_NAME "HSP_WinAPI_Test_Mapping_Object"

; ミューテックスオブジェクトの名前の定義
#define MUTEX_NAME   "HSP_WinAPI_Test_Mutex_Object"

; ミューテックスオブジェクトの作成
hMutex = CreateMutex(0, 0, MUTEX_NAME)

; 待機関数を実行 (排他制御)
WaitForSingleObject hMutex, INFINITE

; ファイルマッピングオブジェクトの作成
sharedMemSize = 1024
hMapObj = CreateFileMapping(-1,0,PAGE_READWRITE,0,sharedMemSize,MAP_OBJ_NAME)

; ファイルマッピングオブジェクトが作成されていたかどうかの判別
if (GetLastError() == ERROR_ALREADY_EXISTS) {
    ; すでに同じ名前のオブジェクトが存在する
    goto *SecondRunProc
} else {
    ; オブジェクトが新しく作成された
    goto *FirstRunProc
}

*FirstRunProc
; プログラム終了時処理の登録
onexit gosub *OnAppQuit

; 割り込み処理メッセージの登録
oncmd gosub *OnRunNotify, MYWM_RUN_NOTIFY

; ビューのマッピング
sharedMemPtr = MapViewOfFile(hMapObj,FILE_MAP_WRITE,0,0,sharedMemSize)

; ウィンドウハンドルを共有メモリに格納
dupptr sharedMemVal, sharedMemPtr, sharedMemSize
sharedMemVal(0) = hwnd

; ビューのマッピング解除
UnmapViewOfFile sharedMemPtr

sharedMemPtr = 0
dim sharedMemVal, 1

; ミューテックスオブジェクトの所有権を解放
ReleaseMutex hMutex
stop

*OnRunNotify
; 二重起動通知メッセージを受け取ったときの処理
gsel 0, 2
dialog "起動されました"
gsel 0, 1
return

*OnAppQuit
; ファイルマッピングオブジェクトとミューテックスのクローズ(終了時)
CloseHandle hMapObj
CloseHandle hMutex
end

*SecondRunProc
; ビューのマッピング
sharedMemPtr = MapViewOfFile(hMapObj,FILE_MAP_READ,0,0,sharedMemSize)

; 共有メモリからウィンドウハンドル取得
dupptr sharedMemVal, sharedMemPtr, sharedMemSize
hwndTarget = sharedMemVal(0)

; ビューのマッピング解除
UnmapViewOfFile sharedMemPtr
sharedMemPtr = 0
dim sharedMemVal, 1

; ミューテックスオブジェクトの所有権を解放
ReleaseMutex hMutex

; メッセージをポスト
PostMessage hwndTarget, MYWM_RUN_NOTIFY, 0, 0

; 対象ウィンドウを前面に表示
SetForegroundWindow hwndTarget

; ファイルマッピングオブジェクトとミューテックスのクローズ
CloseHandle hMapObj
CloseHandle hMutex
end