Mutex よりもロックを優先

前回の投稿で何かが示されたとすれば、ミューテックスは細心の注意を払って使用する必要があるということです。そのため、それらをロックでラップする必要があります。

ロック

ロックは、RAII イディオムに従ってリソースを管理します。ロックは、コンストラクターでミューテックスを自動的にバインドし、デストラクターで解放します。これにより、ランタイムがミューテックスを処理するため、デッドロックのリスクが大幅に軽減されます。

C++11 では、2 つのフレーバーでロックを使用できます。シンプルなユースケースには std::lock_guard、高度なユースケースには std::unique-lock

std::lock_guard

最初は単純な使用例です。

mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();

コードが非常に少ないため、ミューテックス m により、重要なセクション sharedVariable=getVar() へのアクセスがシーケンシャルになります。シーケンシャルとは、この特殊なケースでは、各スレッドが順番にクリティカル セクションにアクセスすることを意味します。コードは単純ですが、デッドロックが発生しやすくなっています。デッドロックは、クリティカル セクションが例外をスローした場合、またはプログラマがミューテックスのロックを解除するのを単に忘れた場合に発生します。 std::lock_guard を使用すると、これをよりエレガントに行うことができます:

{
 std::mutex m,
 std::lock_guard<std::mutex> lockGuard(m);
 sharedVariable= getVar();
}

それは簡単でした。しかし、開き括弧と閉じ括弧はどうでしょうか? std::lock_guard の有効期間は括弧によって制限されます (http://en.cppreference.com/w/cpp/language/scope#Block_scope)。つまり、クリティカル セクションを離れると、その有効期間は終了します。その際、std::lock_guard のデストラクタが呼び出され、ご存知のようにミューテックスが解放されます。これは自動的に発生し、さらに、sharedVariable 内の getVar() =getVar() が例外をスローした場合にも発生します。もちろん、関数本体のスコープまたはループのスコープも、オブジェクトの有効期間を制限します。

std::unique_lock

std::unique_lock は強力ですが、弟の std::lock_guard よりも拡張性があります。

std::unique_lock を使用すると、std::lock_guard に加えて

  • 関連付けられたミューテックスなしで作成
  • ロックされた関連付けられたミューテックスなしで作成
  • 関連付けられたミューテックスのロックを明示的かつ繰り返し設定または解放する
  • ミューテックスを移動
  • ミューテックスをロックしてみてください
  • 関連するミューテックスの遅延ロック

しかし、なぜそれが必要なのですか?ミューテックスのリスクの投稿のデッドロックを覚えていますか?デッドロックの理由は、mutex が異なる順序でロックされていたことです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// deadlock.cpp

#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>

struct CriticalData{
 std::mutex mut;
};

void deadLock(CriticalData& a, CriticalData& b){

 a.mut.lock();
 std::cout << "get the first mutex" << std::endl;
 std::this_thread::sleep_for(std::chrono::milliseconds(1));
 b.mut.lock();
 std::cout << "get the second mutex" << std::endl;
 // do something with a and b
 a.mut.unlock();
 b.mut.unlock();
 
}

int main(){

 CriticalData c1;
 CriticalData c2;

 std::thread t1([&]{deadLock(c1,c2);});
 std::thread t2([&]{deadLock(c2,c1);});

 t1.join();
 t2.join();

}

解決策は簡単です。関数デッドロックは、アトミックな方法でミューテックスをロックする必要があります。それがまさに次の例で起こっていることです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// deadlockResolved.cpp

#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>

struct CriticalData{
 std::mutex mut;
};

void deadLock(CriticalData& a, CriticalData& b){

 std::unique_lock<std::mutex>guard1(a.mut,std::defer_lock);
 std::cout << "Thread: " << std::this_thread::get_id() << " first mutex" << std::endl;

 std::this_thread::sleep_for(std::chrono::milliseconds(1));

 std::unique_lock<std::mutex>guard2(b.mut,std::defer_lock);
 std::cout << " Thread: " << std::this_thread::get_id() << " second mutex" << std::endl;

 std::cout << " Thread: " << std::this_thread::get_id() << " get both mutex" << std::endl;
 std::lock(guard1,guard2);
 // do something with a and b
}

int main(){

 std::cout << std::endl;

 CriticalData c1;
 CriticalData c2;

 std::thread t1([&]{deadLock(c1,c2);});
 std::thread t2([&]{deadLock(c2,c1);});

 t1.join();
 t2.join();

 std::cout << std::endl;

}

引数 std::defer_lock を指定して std::unique_lock のコンストラクターを呼び出した場合、ロックは自動的にロックされません。これは 14 行目と 19 行目で発生します。ロック操作は、23 行目で可変引数テンプレート std::lock を使用してアトミックに実行されます。可変個引数テンプレートは、任意の数の引数を受け入れることができるテンプレートです。ここで、引数はロックです。 std::lock は、アトミック ステップですべてのロックを取得しようとします。それで、彼は失敗するか、それらすべてを取得します。

この例では、std::unique_lock がリソースの有効期間を処理し、std::lock が関連付けられたミューテックスをロックします。しかし、あなたはそれを逆にすることができます。最初のステップでは、mutex をロックします。 2 番目の std::unique_lock では、リソースの有効期間が処理されます。これが 2 番目のアプローチのスケッチです。

std::lock(a.mut, b.mut);
std::lock_guard<std::mutex> guard1(a.mut, std::adopt_lock);
std::lock_guard<std::mutex> guard2(b.mut, std::adopt_lock);

さて、すべて問題ありません。プログラムはデッドロックなしで実行されます。


補足:特別なデッドロック

ミューテックスだけがデッドロックを生成できるというのは幻想です。スレッドがリソースを待機するたびに、スレッドがリソースを保持している間にデッドロックが発生します。

スレッドもリソースです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// blockJoin.cpp

#include <iostream>
#include <mutex>
#include <thread>

std::mutex coutMutex;

int main(){

 std::thread t([]{
 std::cout << "Still waiting ..." << std::endl;
 std::lock_guard<std::mutex> lockGuard(coutMutex);
 std::cout << std::this_thread::get_id() << std::endl;
 }
 );

 {
 std::lock_guard<std::mutex> lockGuard(coutMutex);
 std::cout << std::this_thread::get_id() << std::endl;
 t.join();
 }

}

プログラムはすぐに停止します。

何が起こっていますか?出力ストリーム std::cout のロックと、その子 t に対するメイン スレッドの待機が、デッドロックの原因です。出力を観察することで、ステートメントが実行される順序を簡単に確認できます。

最初のステップで、メイン スレッドは行 19 ~ 21 を実行します。メイン スレッドは、呼び出し t.join() を使用して、その子 t がその作業パッケージで完了するまで、行 21 で待機します。メイン スレッドは、出力ストリームをロックしている間待機しています。しかし、それはまさに子供が待っているリソースです。このデッドロックを解決する 2 つの方法が思い浮かびます。

  • t.join() の呼び出し後、メイン スレッドは出力ストリーム std::cout をロックします。

{
 t.join();
 std::lock_guard<std::mutex> lockGuard(coutMutex);
 std::cout << std::this_thread::get_id() << std::endl;
}
  • メイン スレッドは、追加のスコープによってそのロックを解放します。これは t.join() 呼び出しの前に行われます。

{
 {
 std::lock_guard<std::mutex> lockGuard(coutMutex);
 std::cout << std::this_thread::get_id() << std::endl;
} t.join(); }

次は?

次の投稿では、リーダー/ライター ロックについて説明します。 C++14 以降、リーダー/ライター ロックにより、スレッドの読み取りと書き込みを区別できるようになりました。したがって、任意の数の読み取りスレッドが同時に共有変数にアクセスできるため、共有変数の競合は軽減されます。 (校正者 Alexey Elymanov )