この記事は続き記事です。目次→C++11のthreadで遊んでみる - minus9dの日記
-
- -
mutexを使って排他制御
スレッドを4つ作って、それぞれのスレッドでIDを表示するプログラムを書いてみる。
#include <iostream> #include <thread> #include <vector> void worker() { std::cout << "thread id: " << std::this_thread::get_id() << std::endl; } int main() { // スレッドを4つ作る std::vector<std::thread> ths(4); for (auto& th : ths) { th = std::thread(worker); } // すべてのスレッドが終わるのを待つ for (auto& th : ths) { th.join(); } return 0; }
上のプログラムには欠陥がある。実行結果例は以下。あるスレッドが出力をしている間に他のスレッドが割り込んで表示が乱れてしまった。
thread id: thread id: 0x800103400x800103f8 thread id: 0x80048508 thread id: 0x800104b8
これを防ぐ方法の一つがmutex。mutexを使うと、一つの資源を、各スレッドが専有して使うことができるようになる。そのためには、各スレッドが専有して資源を使用すべき処理の前後を、mutexのlock()とunlock()とで囲えばよい。
#include <iostream> #include <thread> #include <vector> #include <mutex> // グローバル領域にmtxを用意 std::mutex mtx; void worker() { // 共通の資源(ここでは標準出力)を使う前にロックをかける mtx.lock(); // 資源を使った処理を行う std::cout << "thread id: " << std::this_thread::get_id() << std::endl; // 使い終わったらロックを外す mtx.unlock(); } int main() { // スレッドを4つ作る std::vector<std::thread> ths(4); for (auto& th : ths) { th = std::thread(worker); } // すべてのスレッドが終わるのを待つ for (auto& th : ths) { th.join(); } return 0; }
例えるなら、mtxは「使用中」を表すランプみたいなもの。あるスレッドがmtxの様子を見に行き、mtxのランプが消えていれば、mtxのランプを付けて、他のスレッドに今から自分が仕事をするので邪魔をしないでくれと表明する。他のスレッドは、mtxのランプが付いている間はランプが消えるまで待つしかない。あるスレッドが仕事を終えたら、ランプを消して、他のスレッドに席を譲る。
出力例は以下。乱れなく表示できた。
thread id: 0x80010340 thread id: 0x800103f8 thread id: 0x80058540 thread id: 0x800104e8
しかしこの書き方には良くないところがある。上のコードでは間違いなくlock()とunlock()を行ったので問題はなかったが、もしうっかりとunlock()を忘れてしまうとプログラムが固まってしまう。以下はダメな例。
void worker() { // 共通の資源(ここでは標準出力)を使う前にロックをかける mtx.lock(); // 資源を使った処理を行う std::cout << "thread id: " << std::this_thread::get_id() << std::endl; // エラー! mtx.unlock()を呼び忘れているので他のスレッドの処理が止まってしまう }
そこでlock_guardを使うのがよい。lock_guardを使うと、プログラマが明示的にlock()とunlock()を呼ぶ必要がなくなる。
void worker() { // lock_guardを使うと、スコープの終わりでlock()変数が破棄されるのにともなって、自動的にロックも解除される std::lock_guard<std::mutex> lock(mtx); std::cout << "thread id: " << std::this_thread::get_id() << std::endl; }