C++0x の std::unique_lock - std::defer_lock と std::adopt_lock でちょっと高度なロック管理

http://cpplover.blogspot.com/2011/07/mutex.html
C++ソースコード中に「途中のreturnや例外に気をつけること」などといったコメントはあってはならないため,補足.

// 排他的にアクセスするリソース
class exclusive_resource
{
public :
    std::vector<int> v ;
private :
    std::mutex m ;
public :
    void lock() { m.lock() ; }
    void try_lock() { m.try_lock() ; }
    void unlock() { m.unlock() ; }
} ;

exclusive_resource res1, res2 ;

void thread1()
{// res1のみを操作
    std::lock_guard< exclusive_resource > guard( res1 ) ;
    res1.v.push_back(0) ;
}

void thread2()
{// res2のみを操作
    std::lock_guard< exclusive_resource > guard( res2 ) ;
    res2.v.push_back(0) ;
}

void thread3()
{// res1, res2両方を操作
    std::unique_lock<exclusive_resource> lk1(res1, std::defer_lock);
    std::unique_lock<exclusive_resource> lk2(res2, std::defer_lock);
    std::lock( lk1, lk2 ) ; // デッドロックを回避するためstd::lockを使用すること
    // res1, res2両方を操作
    // このスコープをどのように抜けようが res1.unlock() と res2.unlock() が確実に実行される.
}

void thread4()
{// thread3と同じく、res1, res2の両方を操作、thread3とは異なる処理
    std::lock( res1, res2 ) ; // デッドロックを回避するためstd::lockを使用すること
    std::unique_lock<exclusive_resource> lk1(res1, std::adopt_lock);
    std::unique_lock<exclusive_resource> lk2(res2, std::adopt_lock);
    // res1, res2両方を操作
    // このスコープをどのように抜けようが res1.unlock() と res2.unlock() が確実に実行される.
}

std::unique_lock クラステンプレートの基本的な役割,つまりどのような形でスコープを抜けようともロックの解除を確実に行うという RAII のお手本のような役割についてはすでに理解しているものとします.

まず最初に補足しておくと, unique_lock が操作の対象とする型は std::mutex だけではありません.一般に, lockunlock という名前のメンバ関数を持った型――BasicLockable コンセプトのモデル――ならなんでも操作の対象にできます.上のソースコードの例で言えば exclusive_resource クラスはこの要件を満たしているので unique_lock で扱うことができます. exclusive_resource クラスを対象にした場合も,当然, unique_lock が果たす基本的な機能は「デストラクタで exclusive_resource クラスのオブジェクトの unlock メンバ関数を呼び出す」になります.

さて, unique_lock には BasicLockable コンセプトのモデルのオブジェクトを取るだけの基本的なコンストラクタ以外に,やや特殊なコンストラクタがあります.以下の2つのシグネチャを持つ特殊なコンストラクタが今回説明するものです.

namespace std {
template <class Mutex>
class unique_lock {
public:
typedef Mutex mutex_type;
.....
unique_lock(mutex_type& m, defer_lock_t) noexcept;
unique_lock(mutex_type& m, adopt_lock_t);
.....
}
}

これら2つのコンストラクタの2つ目の引数は機能が異なるコンストラクタを呼び分けるための「タグ」を渡すためだけにあります.これらのタグ, std::defer_lock_t のオブジェクト,および std::adopt_lock_t のオブジェクトが 標準ヘッダで次のように宣言されているため

namespace std {
constexpr defer_lock_t defer_lock { };
constexpr adopt_lock_t adopt_lock { };
}

これらのコンストラクタは最初に掲げたソースコード中で示したように std::defer_lock オブジェクトおよび std::adopt_lock オブジェクトを用いて呼びだすことができます(これらのタグは, new に対する std::nothrow_t, std::nothrow と同様の機能を果たすものだと説明すれば理解される方もいらっしゃるかもしれません).

1つ目の defer_lock を渡して呼び出す方のコンストラクタですが,これは「unique_lock に対象のオブジェクトを管理させるが,コンストラクタの段階ではロックを行わない」というものです. "defer" は「保留する」とか「(実行などを)引き延ばす」などといった意味ですね.作成された unique_lock オブジェクトに対して,後に lock メンバ関数が呼ばれてロックされた状態になることでしょう.従って,最初に掲げたソースコード中のように,複数の unique_lockロックを実行せずに作成しておいて最後に std::lock でそれらすべてをデッドロックしないアルゴリズムで安全かつ一斉にロックする,という使い方が可能になります.

2つ目の adopt_lock を渡して呼び出す方のコンストラクタですが,これは「すでにロックされた何らかのリソースについて,ロック解放の管理を unique_lock へ引き継ぐ」という機能を有します.つまり,このコンストラクタはすでにロックされたlock メンバ関数が呼び出されていて,かつ, unlock メンバ関数が呼び出されていない)オブジェクトを第1引数に取り,コンストラクタの呼び出し時にはロックせず(すでにロックされているオブジェクトが渡されることを前提にしているので当然ですね),以降,ロック解放の管理が unique_lock に移譲されることになります.このコンストラクタで作成された unique_lock オブジェクトは当然ながらすでにロックされた状態となります.従って,最初に掲げたソースコード中のように, std::lock で複数のリソースをロックした後に各々のリソースのロック解放の管理を行う unique_lock を作成する,という使い方が可能になります.

このような unique_lock の使い方によって, std::lock を使ってもきちんと RAII を活かしたロック管理が可能になり,ソースコード中に「途中のreturnや例外に気をつけること」などといったコメントを書く必要がなくなります.