yohhoyの日記

技術的メモをしていきたい日記

mutexの制約とバイナリセマフォ

C++11標準ライブラリのstd::mutexオブジェクトでは、ロック取得/解放を同一スレッド*1から行わなければならない。(N3337 30.2.5.2/p3)

m.unlock()
Requires: The current execution agent shall hold a lock on m.

この制約条件のため、下記コードのmutexオブジェクト利用は未定義動作(undefined behavior)を引き起こす。

#include <thread>
#include <mutex>

std::mutex mtx;  // mutexオブジェクト
int resource;    // 保護対象のリソース

void proc()
{
  // resourceを使う処理B
  mtx.unlock();  // (2) NG:別スレッドでロック解放...したい
  //...
}

int main()
{
  mtx.lock();  // (1) メインスレッドでロック獲得
  // resourceを使う処理A
  std::thread th(proc);
  //...
}

一般的にミューテックスは「スレッドにより所有」される設計となっているため*2、ロック所有権の取得/解放を異なるスレッドから行うことは出来ない。POSIX(pthread)、Windows API、Boost.Threadライブラリそれぞれのミューテックスでも同じ制約が存在する*3

この問題はセマフォ(semaphore)を利用して回避できる。特にmutex相当の排他制御を行うだけであれば、バイナリセマフォ(binary semaphore)で十分目的を果たせる。

binsem sem;    // binary semaphoreオブジェクト
int resource;  // 保護対象のリソース

void proc()
{
  // resourceを使う処理B
  sem.unlock();  // (2) 別スレッドで解放(V操作)
  //...
}

int main()
{
  sem.lock();  // (1) メインスレッドで獲得(P操作)
  // resourceを使う処理A
  std::thread th(proc);
  //...
}

上記コードではmutexの代わりにバイナリセマフォbinsemを用いて排他制御を行っている。ただし、resourceを使う処理A, Bから例外スローされた場合はセマフォ解放処理が行われず、同セマフォを獲得しようとした他スレッドが永遠にブロックされる危険性がある。binsemクラスがLockable型の要件*4を満たしていれば、std::unique_lockによるロック保持機構で例外安全なコードを記述できる。

void proc(std::unique_lock<binsem> lk)
{
  // resourceを使う処理B
  lk.unlock();  // (2) 別スレッドで解放
  //...
}

int main()
{
  std::unique_lock<binsem> lk;  // (1) メインスレッドで獲得
  // resourceを使う処理A
  std::thread th(proc, std::move(lk));
  //...
}

C++11標準ライブラリを用いたバイナリセマフォbinsemクラスの実装は下記の通り。同クラスはLockable要件を満たす。

*1:ロックに関するC++11仕様においては "thread" ではなく、より一般化した "execution agent" という用語が用いられる。"An execution agent is an entity such as a thread that may perform work in parallel with other execution agents."(N3337 30.2.5.1/p1)

*2:一般定義についてはwikipedia:en:Mutual_exclusionwikipedia:ja:排他制御あたりを参照。また、ここでは同一プロセス内のスレッド間排他制御のみを対象としている。

*3:Windows APIではCriticalSectionとMutexを異なる同期プリミティブとして扱うが、本記事の文脈では両者は同じ排他制御を実現する機能とみなせる。むしろWindows APIでのクリティカルセクション(critical section)の用法が一般定義と異なり混乱を招いている...

*4:N3337 30.2.5にてBasicLockable, Lockable, TimedLockableの3種類が定義される。各要件には包含関係があり、後者ほど他機能。