Search on the blog

2015年4月19日日曜日

C++マルチスレッド入門

まえがき
先日のCode Jamで並列処理を行えばゴリ押しで解ける問題が出題された。
本番中ゴリ押し解を思いつくには思いついたのだが、C++でマルチスレッドの処理を書いたことが無くて、ごにょごにょやってるうちにタイムアップとなってしまった。

 せっかくなので、C++でマルチスレッドのプログラムの書き方を勉強してみた。後で見返した時にすぐ思い出すように簡単にまとめておく。
  1. マルチスレッド版Hello, World
  2. 子スレッドに引数を渡す
  3. mutexを使ったロック
  4. 子スレッドから結果を受け取る
  5. 子スレッドに後から引数を渡す

1. マルチスレッド版Hello, World
threadクラスのインスタンスを作成すると、スレッドが作成される。
スレッド化したい処理はコンストラクタで渡す。
コンストラクタに渡す処理は、関数でもファンクタでもラムダ式でもよい。
threadインスタンスのjoin()を呼ぶとスレッドの完了を待つ。
detachを呼ぶとスレッドの完了を待たずにmainは終了する。
#include <iostream>
#include <thread>

using namespace std;

int main(int argc, char **argv) {
    
    thread t1([]() { cout << "Hello, World!" << endl; });
    
    // do other tasks

    t1.join();       // wait t1 to finish
    //t1.detach();   // do not wait t1 to finish

    return 0;
}

2. 子スレッドに引数を渡す
スレッド化したい処理に引数がある場合は、threadのコンストラクタの第2引数以降で指定する。
引数を参照で渡したい場合は、refを使って参照ラッパーを渡す。
#include <iostream>
#include <thread>
#include <vector>

using namespace std;

void sayHello(vector<string> &names) {
    for (auto &x : names)
        cout << "Hello, "<< x << "." << endl;
}

int main(int argc, char **argv) {
    
    vector<string> names{ 
        "taro", 
        "hanako", 
        "jiro", 
        "mika"
    };

    thread t1(sayHello, ref(names));

    t1.join();

    return 0;
}
3. mutexを使ったロック
複数スレッドで共有するリソースを使う場合はmutexで排他制御を行う。
mutexクラスのlock、unlockメソッドを直接使うのは非推奨。RAIIでmutexの管理を行う標準ラッパークラスが提供されているのでそれを使う。
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>

using namespace std;

mutex mtx_stdout;

void work(int t) {

    {
        lock_guard<mutex> guard(mtx_stdout);  // RAII
        cout << "work: " << t << " begin." << endl;
    }

    //....
    this_thread::sleep_for (chrono::seconds(3));
    
    {
        lock_guard<mutex> guard(mtx_stdout);  // RAII
        cout << "work: " << t << " end." << endl;
    }

}

int main(int argc, char **argv) {

    vector<thread> threads(10);

    for (int i = 0; i < 10; i++)
        threads[i] = thread(work, i+1);
    
    for (auto &x : threads)
        x.join();
    
    return 0;
}
より柔軟な制御を行いたい場合は、unique_lockを使うという選択肢もある。
void work(int t) {
    unique_lock<mutex> locker(mtx_stdout, defer_lock);

    locker.lock();
    cout << "work: " << t << " begin." << endl;
    locker.unlock();

    //....
    this_thread::sleep_for (chrono::seconds(3));
    
    locker.lock();
    cout << "work: " << t << " end." << endl;
    locker.unlock();
}

5. 子スレッドから結果を受け取る
子スレッドから処理結果を受け取りたい場合は、futureを使う。
asyncの第一引数にlaunch::asyncを指定することで別スレッドで処理を開始出来る。
第一引数にlaunch::deferredを指定した場合は、処理が結果取得時まで遅延される。(※別スレッドは作成されないことに注意)
#include <future>
#include <vector>
#include <iostream>

using namespace std;

bool isPrime(long long n) {
    if (n < 2)
        return false;
    
    for (long long i = 2; i * i <= n; i++)
        if (n % i == 0)
            return false;
    
    return true;           
}

int main(int argc, char **argv) {

    future<bool> fut = async(launch::async, isPrime, 1000000007);
    cout << fut.get() << endl;

    return 0;
}
6. 子スレッドに後から引数を渡す
スレッド実行時に引数の値が分かっておらず、後から非同期で子スレッドに引数を渡したい場合はpromiseを使う。
#include <future>
#include <vector>
#include <iostream>

using namespace std;

bool test(int x, future<int> &f) {
    return x < f.get();
}

int main(int argc, char **argv) {

    promise<int> p;
    future<int> f = p.get_future();

    future<bool> fut = async(launch::async, test, 10, ref(f));

    p.set_value(12);
    cout << fut.get() << endl;

    return 0;
}

0 件のコメント:

コメントを投稿