この記事の最初の部分では、C ++ 11のスレッドとロックに焦点を当て、すべての栄光の条件変数について、
2番目の部分で詳しく説明します...
ストリーム
C ++ 11では、スレッドの処理は
std::thread
クラス(
<thread>
ヘッダーファイルからアクセス可能)を使用して実行され
std::thread
クラスは、通常の関数、ラムダ、およびファンクターで動作できます。 さらに、任意の数のパラメーターをストリーム関数に渡すことができます。
#include <thread> void threadFunction() { // do smth } int main() { std::thread thr(threadFunction); thr.join(); return 0; }
この例では、
thr
は
threadFunction()
関数が実行されるスレッドを表すオブジェクトです。
join
呼び出しは、
thr
(または、むしろ
threadFunction()
)がジョブを
threadFunction()
まで、呼び出しスレッド(この場合はメインスレッド)をブロックします。 ストリーム関数が値を返す場合、値は無視されます。 ただし、任意の数のパラメーターが関数を受け入れることができます。
void threadFunction(int i, double d, const std::string &s) { std::cout << i << ", " << d << ", " << s << std::endl; } int main() { std::thread thr(threadFunction, 1, 2.34, "example"); thr.join(); return 0; }
任意の数のパラメーターを渡すことができるという事実にもかかわらず、それらはすべて値で渡されます。パラメーターを参照で関数に渡す必要がある場合は、例のように
std::ref
または
std::cref
でラップする必要があります。
void threadFunction(int &a) { a++; } int main() { int a = 1; std::thread thr(threadFunction, std::ref(a)); thr.join(); std::cout << a << std::endl; return 0; }
プログラムはコンソール2に出力されます
std::ref
使用しない場合、プログラムの結果は1になります。
join
方法に加えて、別の同様の方法
detach
検討する必要があります。
detach
使用すると、ストリームをオブジェクトから切り離す、つまりバックグラウンドにすることができます。
join
スレッドは、切断されたスレッドに適用できなくなりました。
int main() { std::thread thr(threadFunction); thr.detach(); return 0; }
また、スレッド関数が例外をスローした場合、try-catchブロックによってキャッチされないことに注意する必要があります。 つまり 次のコードは機能しません(より正確に機能しますが、意図したとおりではありません:例外をキャッチせずに):
try { std::thread thr1(threadFunction); std::thread thr2(threadFunction); thr1.join(); thr2.join(); } catch (const std::exception &ex) { std::cout << ex.what() << std::endl; }
スレッド間で例外を渡すには、ストリーム関数でそれらをキャッチし、将来それらにアクセスするためにどこかに保存する必要があります。
std::mutex g_mutex; std::vector<std::exception_ptr> g_exceptions; void throw_function() { throw std::exception("something wrong happened"); } void threadFunction() { try { throw_function(); } catch (...) { std::lock_guard<std::mutex> lock(g_mutex); g_exceptions.push_back(std::current_exception()); } } int main() { g_exceptions.clear(); std::thread thr(threadFunction); thr.join(); for(auto &e: g_exceptions) { try { if(e != nullptr) std::rethrow_exception(e); } catch (const std::exception &e) { std::cout << e.what() << std::endl; } } return 0; }
先に
std::this_thread
に、
std::this_thread
<thread>
が提供する便利な機能をいくつか指摘して
std::this_thread
。
ロック
最後の例では、
g_exceptions
ベクトルへのアクセスを同期して、
g_exceptions
1つのスレッドのみが新しい要素を挿入できるようにする必要がありました。 このために、ミューテックスを使用し、ミューテックスをロックします。 Mutexは同期の基本要素であり、C ++ 11では、
<mutex>
ヘッダーファイルに4つの形式で示されます。
前述のヘルパー
get_id()
および
sleep_for()
std::mutex
を使用する例を次に示します。
#include <iostream> #include <chrono> #include <thread> #include <mutex> std::mutex g_lock; void threadFunction() { g_lock.lock(); std::cout << "entered thread " << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(rand()%10)); std::cout << "leaving thread " << std::this_thread::get_id() << std::endl; g_lock.unlock(); } int main() { srand((unsigned int)time(0)); std::thread t1(threadFunction); std::thread t2(threadFunction); std::thread t3(threadFunction); t1.join(); t2.join(); t3.join(); return 0; }
プログラムは次のように表示されます。
entered thread 10144 leaving thread 10144 entered thread 4188 leaving thread 4188 entered thread 3424 leaving thread 3424
一般データにアクセスする前に、mutexを
lock
メソッドでロックし、一般データでの作業が終了したら、unlockメソッドでロック解除する必要があります。
次の例は、1つの要素を追加
add()
メソッドと複数の要素を追加する
addrange()
メソッドを持つ単純なスレッドセーフコンテナ(
std::vector
基づいて実装)を示しています。
注 :それでも、このコンテナは、
va_args
の使用など、いくつかの理由で完全にスレッドセーフではありません。 また、
dump()
メソッドはコンテナに属しているのではなく、スタンドアロン関数である必要があります。 この例の目的は、ミューテックスを使用する基本的な概念を示すことであり、完全でエラーのないスレッドセーフなコンテナを作成することではありません。
template <typename T> class container { std::mutex _lock; std::vector<T> _elements; public: void add(T element) { _lock.lock(); _elements.push_back(element); _lock.unlock(); } void addrange(int num, ...) { va_list arguments; va_start(arguments, num); for (int i = 0; i < num; i++) { _lock.lock(); add(va_arg(arguments, T)); _lock.unlock(); } va_end(arguments); } void dump() { _lock.lock(); for(auto e: _elements) std::cout << e << std::endl; _lock.unlock(); } }; void threadFunction(container<int> &c) { c.addrange(3, rand(), rand(), rand()); } int main() { srand((unsigned int)time(0)); container<int> cntr; std::thread t1(threadFunction, std::ref(cntr)); std::thread t2(threadFunction, std::ref(cntr)); std::thread t3(threadFunction, std::ref(cntr)); t1.join(); t2.join(); t3.join(); cntr.dump(); return 0; }
このプログラムが実行されると、
デッドロックが発生します(デッドロック、つまりブロックされたスレッドはまだ待機します)。 理由は、コンテナーがリリースされる前に(
unlock
呼び出す)ミューテックスを数回取得しようとするためです。これは不可能です。 ここで
std::recursive_mutex
がシーンに入り、同じミューテックスを数回取得できます。 ミューテックスを受け取る最大量は定義されていませんが、この量に達すると、
lock
は例外
std :: system_errorをスローします。 したがって、上記のコードの問題に対する解決策は
addrange()
lock
と
unlock
呼び出されないように
addrange()
実装を変更することを除く)、ミューテックスを
std::recursive_mutex
に置き換えることです。
template <typename T> class container { std::recursive_mutex _lock;
これで、プログラムの結果は次のようになります。
6334 18467 41 6334 18467 41 6334 18467 41
threadFunction()
を呼び出すと、同じ番号が生成されることに気づいたでしょう。 これは、関数
void srand (unsigned int seed);
メインスレッドの
seed
のみを初期化します。 他のスレッドでは、擬似乱数ジェネレーターは初期化されず、毎回同じ番号が取得されます。
明示的なロックおよびロック解除は、たとえば、スレッドのロック解除を忘れた場合や、逆にロックの順序が間違っている場合など、エラーにつながる可能性があります。これはすべてデッドロックを引き起こします。 Stdは、この問題を解決するためのいくつかのクラスと関数を提供します。
ラッパークラスを使用すると、単一のブロック内で自動ロックおよびロック解除を行う
RAIIスタイルのミューテックスを一貫して使用できます。 これらのクラスは次のとおりです。
- lock_guard :オブジェクトが作成されると、(
lock()
呼び出すことによりlock()
ミューテックスを取得しようとし、オブジェクトが破棄されると、( unlock()
呼び出すことによりunlock()
ミューテックスを自動的に解放します - unique_lock : lock_guardとは異なり、遅延ロック、一時ロック、再帰的ロック、条件変数の使用もサポートします
これを念頭に置いて、コンテナクラスを次のように書き換えることができます。
template <typename T> class container { std::recursive_mutex _lock; std::vector<T> _elements; public: void add(T element) { std::lock_guard<std::recursive_mutex> locker(_lock); _elements.push_back(element); } void addrange(int num, ...) { va_list arguments; va_start(arguments, num); for (int i = 0; i < num; i++) { std::lock_guard<std::recursive_mutex> locker(_lock); add(va_arg(arguments, T)); } va_end(arguments); } void dump() { std::lock_guard<std::recursive_mutex> locker(_lock); for(auto e: _elements) std::cout << e << std::endl; } };
dump()
メソッドは、コンテナの状態を変更しないため、定数である必要があると言えます。 次のようにして、コンパイルエラーを取得してください。
'std::lock_guard<_Mutex>::lock_guard(_Mutex &)' : cannot convert parameter 1 from 'const std::recursive_mutex' to 'std::recursive_mutex &'
ミューテックス(実装の形式に関係なく)を受信およびリリースする必要があり、これは非定数メソッド
lock()
および
unlock()
の使用を意味します。 したがって、
lock_guard
引数を定数にすることはできません。 この問題の解決策は、ミューテックスを
mutable
にすることです。その後、const指定子は無視され、定数関数から状態を変更できます。
template <typename T> class container { mutable std::recursive_mutex _lock; std::vector<T> _elements; public: void dump() const { std::lock_guard<std::recursive_mutex> locker(_lock); for(auto e: _elements) std::cout << e << std::endl; } };
ラッパークラスのコンストラクターは、ロックポリシーを定義するパラメーターを取ることができます。
- タイプ
defer_lock_t
:ミューテックスを受信しません try_to_lock
型のtry_to_lock_t
:ロックせずにミューテックスを取得しよう- タイプ
adopt_lock_t
:呼び出しスレッドにすでにmutexがあると想定されます
次のように宣言されます。
struct defer_lock_t { }; struct try_to_lock_t { }; struct adopt_lock_t { }; constexpr std::defer_lock_t defer_lock = std::defer_lock_t(); constexpr std::try_to_lock_t try_to_lock = std::try_to_lock_t(); constexpr std::adopt_lock_t adopt_lock = std::adopt_lock_t();
ミューテックスの「ラッパー」に加えて、
std
は1つ以上のミューテックスをロックするためのいくつかのメソッドも提供します。
- lock :デッドロック回避アルゴリズムを使用してmutexをロックします(
lock()
、 try_lock()
およびunlock()
) - try_lock :mutexを指定された順序でブロックしようとします
デッドロックの典型的な例を次に示します。要素を持つコンテナと、異なるコンテナの2つの要素を
exchange()
する
exchange()
関数があります。 スレッドセーフのために、関数はこれらのコンテナへのアクセスを同期し、各コンテナに関連付けられたミューテックスを受け取ります。
template <typename T> class container { public: std::mutex _lock; std::set<T> _elements; void add(T element) { _elements.insert(element); } void remove(T element) { _elements.erase(element); } }; void exchange(container<int> &c1, container<int> &c2, int value) { c1._lock.lock(); std::this_thread::sleep_for(std::chrono::seconds(1));
この関数が最初のスレッドから2つの異なるスレッドから呼び出されると仮定します。要素は1コンテナから削除され、2に追加されます。2番目のスレッドから要素が2コンテナから削除され、1に追加されます。これにより、デッドロックが発生する可能性があります(スレッドのコンテキストが切り替わる場合)あるスレッドから別のスレッドへ、最初のブロックの直後)。
int main() { srand((unsigned int)time(NULL)); container<int> cntr1; cntr1.add(1); cntr1.add(2); cntr1.add(3); container<int> cntr2; cntr2.add(4); cntr2.add(5); cntr2.add(6); std::thread t1(exchange, std::ref(cntr1), std::ref(cntr2), 3); std::thread t2(exchange, std::ref(cntr2), std::ref(cntr1), 6); t1.join(); t2.join(); return 0; }
この問題を解決するには、
std::lock
使用し
std::lock
。これにより、(デッドロックに関して)安全な方法でロックが保証されます。
void exchange(container<int> &c1, container<int> &c2, int value) { std::lock(c1._lock, c2._lock); c1.remove(value); c2.add(value); c1._lock.unlock(); c2._lock.unlock(); }
続き :
条件変数