LoginSignup
16
21

More than 3 years have passed since last update.

C++11のThreadを使ってRead-Write Lockパターン

Last updated at Posted at 2018-01-06

概要

以前,pthreadを使ってJavaライクなスレッドライブラリを作ったものの,c++11からネイティブでThreadがサポートされていることを後から知った.そこで,ネイティブのThreadの使い方を学習しつつ,Read-Write Lockパターンを再度実装してみる.

C++のThreadライブラリ

C++11からのスレッドライブラリを使うには,#include<thread>を追加した上で,コンストラクタにスレッドで実行すべき関数ポインタ,あるいは関数オブジェクトを渡す.

void foo() {}

std::thread t(foo)

以下のコードを実行すると

0.cpp
#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::mutexlock()/unlock()が利用できる.
次のコードを実行すると

1.cpp
#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::mutexlock/unlockメソッドを明示的に呼び出してロックの取得と開放を行っていた.C++11からは,ロックの取得をスコープアウトのタイミングで自動的にやってくれるクラスが追加されている.
- lock_guard:このオブジェクトが生成されたタイミングでロックを確保し,削除される(スコープアウト)ときに開放する
- unique_locklock_guardよりも便利なメソッドを備えたクラス.具体的には,任意のタイミングでロックを確保したりできる.これを使うと,1.cppのworkerは次のように書き直せる.

lock_guard
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メソッドを備えたクラスであれば良いらしい.

unique_lock
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で書くとこうなる.

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を使っている.

condition_variable
#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に関してはこちらが詳しい.せっかくなので,ちょっとやってみました.

raii.cpp
#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とか使うのだろう..まだまだ修行が足りません.

全ソースコードはこちら

参考文献

16
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
21