C ++ 11のストリーム、ロック、および条件変数[パート1]

この記事の最初の部分では、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スタイルのミューテックスを一貫して使用できます。 これらのクラスは次のとおりです。



これを念頭に置いて、コンテナクラスを次のように書き換えることができます。

 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; } };
      
      





ラッパークラスのコンストラクターは、ロックポリシーを定義するパラメーターを取ることができます。



次のように宣言されます。

 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つ以上のミューテックスをロックするためのいくつかのメソッドも提供します。



デッドロックの典型的な例を次に示します。要素を持つコンテナと、異なるコンテナの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)); //  deadlock c2._lock.lock(); c1.remove(value); c2.add(value); c1._lock.unlock(); c2._lock.unlock(); }
      
      





この関数が最初のスレッドから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(); }
      
      





続き条件変数



All Articles