2012年1月15日日曜日

Windows 上での Boost.Interprocess の named_mutex の挙動

自分が改造して公開している ax7z_s.spi のα版では(必要もないのですが) Boost.Interprocess をほんの一部だけ使用しています。その中でちょっと想定外の挙動があったのでメモしてみます。使用しているのは named_mutex です。Windows 上だと Mutex に対するラッパになっている……と思っていたのですが実はそうなっていません(少なくとも 1.48 までは)。共有メモリを使ってエミュレーションされています。では、それがどのような違いを生むのか簡単に見てみましょう。まずは Win32 API 版。

Locker by Win32 API

#include <windows.h>
#include <iostream>

int main(void)
{
 HANDLE hMutex = CreateMutex(0, TRUE, "cx.myhome.yak.test"); // Initial owner
 char c; std::cin >> std::noskipws >> c;
 CloseHandle(hMutex);
 return 0;
}

Waiter by Win32 API

#include <windows.h>
#include <iostream>

int main(void)
{
 HANDLE hMutex = CreateMutex(0, FALSE, "cx.myhome.yak.test");
 char c; std::cin >> std::noskipws >> c;
 DWORD dwResult = WaitForSingleObject(hMutex, INFINITE);
 std::cout << dwResult << std::endl;
 ReleaseMutex(hMutex);
 CloseHandle(hMutex);
 return 0;
}

Locker 起動→Waiter 起動→Locker に ENTER 入力→Waiter で ENTER 入力、した場合の挙動は次の図のようになります。

Locker が Mutex を獲得したまま終了しますが、この時点で Wait している Waiter 側には WAIT_ABANDONED(256) が返ってきます。Mutex を獲得していたプロセスが死んだことを通知するための値ですが、WAIT_OBJECT_0(0) が返ってきたかのように無視すればそのまま動作します。一方、Locker 起動→Locker に ENTER 入力→Waiter 起動→Waiter で ENTER 入力、した場合の挙動は次の図のようになります。

こちらでも Locker が Mutex を獲得したまま終了しますが、この時点で Mutex に対するハンドルがないので Mutex オブジェクトが消滅します(多分)。結果、Locker を起動した際には何事もなく WAIT_OBJECT_0(0) が返ってきます。

いずれにしろ、Locker 側が Mutex を獲得したまま終了しても、Waiter 側は問題なく実行できる、というところがポイントです。

それでは Boost Interprocess を使った場合はどうなるでしょうか?

Locker by Boost.Interprocess

#include <boost/interprocess/sync/named_mutex.hpp>
#include <iostream>

int main()
{
 using namespace boost::interprocess;
 named_mutex mutex(open_or_create, "cx.myhome.yak.test");
 mutex.lock();
 char c; std::cin >> std::noskipws >> c;
 return 0;
}

Waiter by Boost.Interprocess

#include <boost/interprocess/sync/named_mutex.hpp>
#include <iostream>

int main()
{
 using namespace boost::interprocess;
 named_mutex mutex(open_or_create, "cx.myhome.yak.test");
 char c; std::cin >> std::noskipws >> c;
 mutex.lock();
 mutex.unlock();
 return 0;
}

Locker 起動→Waiter 起動→Locker に ENTER 入力→Waiter で ENTER 入力、した場合の挙動は次の図のようになります。

Locker が Mutex を獲得したまま終了しますが、Win32 API の場合と異なり獲得された状態のまま残ってしまいます。そのため、Locker はデッドロックします。ちゃんと解放するように組めばいい、と思われるかもしれませんが、強制終了させた場合なんかもデッドロックしてしまうため結構厳しいです。Locker 起動→Locker に ENTER 入力→Waiter 起動→Waiter で ENTER 入力、した場合の挙動も次のようになります。

こちらの場合でも Mutex は獲得された状態のまま残ってしまい、Locker がデッドロックします。

どうしてこうなるか、というのは named_mutex の実装に由来します。上では共有メモリを使っていると書きましたが正確にはファイルマッピングを使っています。名前との対応関係を容易にするためでしょう実際にファイルと結びつけられたものです。c:\ProgramData あるいは c:\Documents and Settings\All Users\Application Data フォルダ以下に boost_interprocess というフォルダが作成され、さらにその下に 20120102003353.109999 のような名前のフォルダが起動時刻を元に作成されます。この下に指定された名前(上の例の場合なら cx.myhome.yak.test)のファイルができます。これが Mutex の実体です。特に9~12バイト目の部分が Mutex の状態を表しており、1 なら獲得状態、0なら非獲得状態です。このファイルが獲得状態のまま残り続けるためずっとデッドロックすることになります。もちろんこのファイルを削除してから起動してやればロックからは脱出できます。また、再起動後に Waiter 等を実行すると起動時刻を元に作成するフォルダが再作成され古いフォルダは削除されるのでロック状態からは脱出できるようになっています。つまり Boost.Interprocess のデータは破棄しない限り再起動するまでの間有効ということです。

さて、最後にもう一つ。Boost.Interprocess の named_mutex の例の項では remover というものがあります。大抵の C++er は「デストラクタで始末すればいいのになんで別に始末するためのものを用意する必要があるんだろう?」と思われるのではないでしょうか。理由は恐らく「共有されている named_mutex の実体」を削除するタイミングは named_mutex 各オブジェクトからは分からないためだと思われます。デストラクタで行っている理由は例外送出時の対応と、順序(named_mutex オブジェクト破棄後に remove)のため……だと思ったのですが、実は順序は named_mutex 破棄後に remove でなくとも動作します。

#include <boost/interprocess/sync/named_mutex.hpp>
#include <iostream>

int main()
{
 using namespace boost::interprocess;
 named_mutex mutex(open_or_create, "cx.myhome.yak.test");
 mutex.lock();
 char c; std::cin >> std::noskipws >> c;
 named_mutex::remove("cx.myhome.yak.test");
 std::cin >> std::noskipws >> c;
 return 0;
}

上のコードを実行し、boost_interprocess フォルダの中を覗いてみると cx.myhome.yak.test という名前のファイルが出来ておりラスト 4 byte は 1 になっているはずです。 1 回 ENTER を打った後だと named_mutex オブジェクトが存在する状態で remove していることなりますが、cx.myhome.yak.test はなくなり、60FEE8D7B3C8CC018964BC3FCED2CC01 みたいな謎のファイル名に変わっているはずです。これは削除準備状態でこのファイル自体を開くことはできません。元ファイルを参照しているハンドルが閉じられた場合(この場合 named_mutex が破棄された場合)削除されます。ちなみにこの削除準備状態で Waiter を実行して ENTER を打つと cx.myhome.yak.test は存在しないためデッドロックせず問題なく終了します。この後、再度 ENTER を打つと named_mutex が破棄され削除準備状態だったファイルが削除されます。逆に言えば remove は問答無用で実体を削除してしまうため使うタイミングは良く考えるべきだと言えると思います。

さて、長々書いてきましたが、どうやらこれ、POSIX の場合に使用される POSIX semaphore の挙動と一致しているようです。そしてプロセスが死ぬようなケースでは POSIX semaphore を使わず OS が勝手に解放してくれるファイルロック等を使え、ということのようです。参考 URL: