概要
以前,pthreadを使ってJavaライクなスレッドライブラリを作ったものの,c++11からネイティブでThreadがサポートされていることを後から知った.そこで,ネイティブのThreadの使い方を学習しつつ,Read-Write Lockパターンを再度実装してみる.
C++のThreadライブラリ
C++11からのスレッドライブラリを使うには,#include<thread>
を追加した上で,コンストラクタにスレッドで実行すべき関数ポインタ,あるいは関数オブジェクトを渡す.
void foo() {}
std::thread t(foo)
以下のコードを実行すると
#include <thread>
#include <iostream>
void foo() {
std::cout << "Hello Thread" << std::endl;
}
int main(void) {
std::thread t(foo);
t.join();
std::cout << "End of Main" << std::endl;
}
以下の実行結果を得る
Hello Thread
End of Main
threadが実行を開始したら,メインスレッド(この場合mainを実行するスレッド)ではjoin
またはdetach
を呼び出す必要がある.さもないと,エラーで落ちる.ここではjoin
を呼び出して,スレッドの終了を待っている.
引数のある関数をスレッドで実行するには,コンストラクタに与えればいい.
例えば以下を実行すると,
void bar(int *v) {
(*v)++;
}
int main(void) {
int i = 10;
std::thread t(bar, &i);
t.join();
std::cout << "v = " << i << std::endl;
std::cout << "End of Main" << std::endl;
}
以下の結果を得る.
v = 11
End of Main
引数に参照を取る場合には,std::ref
でラップする必要がある.
void goo(int& v) {
v++;
}
int main(void) {
int i = 10;
std::thread t(goo, std::ref(i)); --> std::refでラップ
t.join();
std::cout << "v = " << i << std::endl;
std::cout << "End of Main" << std::endl;
}
仮にラップしない場合,大量のエラーが出る.なお,引数は複数渡すことができる.
mutex
pthread同様,スレッド間の競合を解決するには,std::mutex
のlock()/unlock()
が利用できる.
次のコードを実行すると
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
struct Data {
int data;
std::mutex mtx;
};
void worker(Data& d) {
d.mtx.lock(); // ロック
int n = d.data;
{
// sleep
std::this_thread::sleep_for(std::chrono::seconds(1));
n += 1;
}
d.data = n;
std::cout << "d.data : " << d.data << std::endl;
d.mtx.unlock(); // アンロック
}
int main() {
std::vector<std::thread> ths(4);
Data d;
d.data = 0;
for (std::thread& th : ths) {
th = std::thread(worker, std::ref(d));
}
for (std::thread& th : ths) {
th.join();
}
std::cout << d.data << std::endl;
}
以下の結果を得る.
d.data : 1
d.data : 2
d.data : 3
d.data : 4
4
このコードでは4つのスレッドを生成し,それぞれworker
を実行する.worker
ではロックを取った上でdata
をインクリメントする.その結果,正しくインクリメントできていることがわかる.
lock_guardとunique_lock
先の1.cppでは,std::mutex
のlock/unlock
メソッドを明示的に呼び出してロックの取得と開放を行っていた.C++11からは,ロックの取得をスコープアウトのタイミングで自動的にやってくれるクラスが追加されている.
-
lock_guard
:このオブジェクトが生成されたタイミングでロックを確保し,削除される(スコープアウト)ときに開放する -
unique_lock
:lock_guard
よりも便利なメソッドを備えたクラス.具体的には,任意のタイミングでロックを確保したりできる.これを使うと,1.cppのworker
は次のように書き直せる.
void worker_lock_guard(Data& d) {
{
std::lock_guard<std::mutex> lock(d.mtx); // ロックの取得
int n = d.data;
// sleep
std::this_thread::sleep_for(std::chrono::seconds(1));
n += 1;
d.data = n;
std::cout << "d.data : " << d.data << std::endl;
} // ここで開放
}
lock_guardでは,コンストラクタにテンプレートで指定したオブジェクトの参照を渡す.テンプレートに指定するクラスは,lock/unlock
メソッドを備えたクラスであれば良いらしい.
void worker_unique_lock1(Data& d) {
std::unique_lock<std::mutex> lock(d.mtx, std::defer_lock);
lock.lock(); // ロックの取得
int n = d.data;
// sleep
std::this_thread::sleep_for(std::chrono::seconds(1));
n += 1;
d.data = n;
std::cout << "d.data : " << d.data << std::endl;
} // この関数を抜けるときにロックを開放
lock_guardと同等のことをunique_lockで書くとこうなる.
void worker_unique_lock2(Data& d) {
std::unique_lock<std::mutex> lock(d.mtx, std::defer_lock);
for(;;) {
if (lock.try_lock()) { // ロックの取得を試みる
int n = d.data;
// sleep
std::this_thread::sleep_for(std::chrono::seconds(1));
n += 1;
d.data = n;
std::cout << "d.data : " << d.data << std::endl;
return;
} else { // 取れなかったらCPUリソースを開放
std::this_thread::yield();
}
}
} // この関数を抜けるときにロックを開放
もうちょっと便利な機能(try_lock)を使った例.実行結果は1.cppと同一.
condition_variable
この記事で書いたロックと通知を行う仕組み.std::condition_variable
がモニタになる.そして,通知を待つためにunique_lock
を使っている.
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <iostream>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
void worker1() {
std::cout << "Worker1 start" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
// finish preparing
std::cout << "Worker1 finish" << std::endl;;
{
std::lock_guard<std::mutex> lock(mtx);
}
cv.notify_all(); // cvで待機しているスレッドに通知
}
void worker2(){
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Worker2 start" << std::endl;
{
std::unique_lock<std::mutex> uniq_lk(mtx); // ここでロックされる
cv.wait(uniq_lk); // ここで一旦ロックを開放
} // ここで開放
std::cout << "Worker2 finish" << std::endl;
}
int main()
{
std::thread th1(worker1);
std::thread th2(worker2);
th1.join();
th2.join();
return 0;
}
これを実行すると,以下の結果を得る
Worker1 start // worker1は3秒待つ
Worker2 start // worker2がcv.waitで待つ
Worker1 finish // worker1が処理を終え,待っているスレッドに通知する
Worker2 finish // worker1から通知を受け起きる.
なお,spurious wakeupの対策についてはこちらを参照.
RAII (Resource Acquisition Is Initialization)
すこし横道に逸れるが,lock_guardはRAIIで実現されているのだと想像できる.RAIIに関してはこちらが詳しい.せっかくなので,ちょっとやってみました.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
using namespace std;
struct Data {
int data;
};
class Wlock {
public:
mutex &m;
explicit Wlock(mutex& mtx) : m(mtx) {
this->m.lock(); // コンストラクタでロックする
}
~Wlock() {
this->m.unlock(); // デストラクタでロックを開放
}
};
void bar(Data& d, mutex& m) {
{
Wlock lock(m); // ここでロック
int n = d.data;
this_thread::sleep_for(chrono::seconds(1));
n += 1;
d.data = n;
cout << "d.data : " << d.data << endl;
} // ここでロックを開放
}
int main() {
vector<thread> ths(4);
Data d;
mutex m;
d.data = 0;
for (thread& th : ths) {
th = thread(bar, ref(d), ref(m));
}
for (thread& th : ths) {
th.join();
}
std::cout << d.data << std::endl;
}
lock_guard/unique_lockの例を自作クラス(Wlock
)で実現した例.lock_guardはさらにテンプレートを使って柔軟に書いてあるのだと思う.
Read-Write Lockパターン
前に書いたRead-Write Lockパターンを,C++標準のthreadとlock_guard, condition_variableを使って書き直してみた.
どうやらC++ではポインタをできるだけ見せずに参照でやり取りするほうが良いらしいので,ちょっと意識してみました.動的なオブジェクトを使う必要はあるので,その場合にはRAIIでshared_pointerとか使うのだろう..まだまだ修行が足りません.
全ソースコードはこちら