読者です 読者をやめる 読者になる 読者になる

C++11で始めるマルチスレッドプログラミングその1 ~std::thread事始め~

C++

この記事は、C++11におけるマルチスレッドプログラミング入門記事という位置づけで書かれたものです。簡単のため、表現が曖昧になったりしている部分があると思いますが、もっと厳密に知りたいという方はC++の規格を参照してください。

C++11のマルチスレッドライブラリ

C++03までは、マルチスレッドプログラミングを行うための言語機能やライブラリが標準で用意されていませんでした。そのため、プログラマはしばしばプラットフォームに依存したコードを書く必要がありました。

しかしC++11から、thread-aware memory modelなどの定義や、マルチスレッドをサポートするための言語機能とライブラリが導入されました。これによって、プログラマは抽象度の高いコードを用いてマルチスレッドプログラミングを行うことが容易になりました。

本記事では、事始めとしてstd::threadを用いて簡単なプログラムを書いていきます。また、ソースコードの例ではインクルードファイルを省略しているので、手元の環境でテストしたい方は注意してください。

std::thread事始め

スレッドを新たに作成するためには、std::threadを使います。具体的には、std::threadのコンストラクタで、第一引数に、別スレッドで処理させたい関数や関数オブジェクトを渡し、第二引数以降には第一引数で与えられた関数に適用する引数を渡して新たなスレッドを作成することになります。つまり、std::thread(callable_object, args...)のように引数を与えると、別スレッドでcallable_object(args...)の処理が行われるということになります。
また、std::thread内部では、コンストラクタで作成されたスレッドを保持し、std::threadオブジェクトと関連づいています。
以下が、std::threadを用いたプログラムの例になります。joinとdetachについては後に解説します。

void process() {
    //...
}

int main() {
    int i = 0;
    std::thread t1(process); // 別スレッドでprocessの処理を行う
    std::thread t2([&i](int x) {
        int counter = 0;
        while(counter++ < x) {
            i += counter;
        }
    }, 10); // 関数オブジェクトの引数に10を渡す
    t1.detach(); //スレッドt1の管理を放棄
    t2.join(); //スレッドt2の処理が終了するまで待機
    std::cout << i << std::endl;
}

出力

55

joinとdetach

std::threadを一度作成すると、必ずjoin()またはdetach()を呼ばなければなりません。さもなくば、std::terminateでジ・エンドです。
また、一度join()またはdetach()が呼び出されて、空の状態になったstd::threadに対して、再度join()、detach()を呼び出してはなりません。

join

join()を呼び出すと、join()が呼び出されたスレッド(上の例ではt2)の処理が終了するまで、join()を呼び出したスレッドはブロックされます。上の例だと、t2の処理が終わるまで、iの出力が行われることはありません。
また、join()を呼び出して処理が終了したthreadオブジェクトは、どのスレッドもささない空のthreadオブジェクトになります。
threadオブジェクトがjoin()を呼び出せる状態かどうかは、joinable()メンバ関数で確認できます。

detach

detach()を呼び出すと、threadオブジェクトとスレッドの関連を切り離すことができます。切り離されたthreadオブジェクトは、join()の呼び出し後と同様に、何もささない空のthreadオブジェクトとなります。また、切り離されたスレッドは、そのまま処理が続行されますが、他のスレッドから一切干渉することができなくなります。
threadオブジェクトがdetach()を呼び出せる状態かどうかは、join()同様、joinable()メンバ関数で確認できます。

RAIIを用いてjoin()、detach()の呼び出し忘れを防ぐ

先に述べたとおり、threadは必ずjoin()またはdetach()が呼び出されなければなりません。しかし直にjoin()やdetach()を呼び出すコードは、例外機構との相性が悪いです。例えば、以下のコードではjoinが呼び出されない可能性があります。

std::thread t([] { /*...*/ });
some_process(); // 例外が投げられうる
t.join();       // some_processで例外が投げられると呼び出されない

これを防ぐために、RAIIを用いてjoin()やdetach()を呼び出す仕組みがあります。以下のthread_guardがその一例です*1が、他にもBoostのscoped_guardなどがあるので、調べてみてください。

class thread_guard {
    std::thread& t;
public:
    explicit thread_guard(std::thread& t_) : t(t_) {}
    ~thread_guard() {
        if(t.joinable()) {
            t.join();
        }
    }
    thread_guard(thread_guard const&) = delete;
    thread_guard& operator=(thread_guard const&) = delete;
};

int main() {
    std::thread t1([]{ /* ... */ });
    thread_guard tg(t1);
    some_process();
} // 例外が投げられてもjoinが呼ばれる

threadの委譲

std::threadは、noncopyableかつmovableなクラスです。ムーブすると、スレッドの管理を別のthreadオブジェクトに委譲することができます。

std::thread t1([]{ /* ... */ });
std::thread t2;
// t2 = t1;  Error
t2 = std::move(t1); // ok

ただしこの時、委譲される側のthreadは何もささない空のthreadオブジェクトである必要があります。そうでない場合、std::terminate()が呼ばれます。
委譲する側のthreadは、ムーブの後なにもささない空のthreadオブジェクトになります。

threadの識別

スレッドに関連付けられている各threadは、IDづけがされており、それによって識別することができます。自身のスレッドのIDは、std::this_thread::get_id()を呼び出すことで得られます。
得られるIDの型はstd::thread::idですが、これは自由にコピーしたり、比較したりすることができます。
デバッグに使ったり、mapのkeyに使ったり、いろいろな用途に使えます。
スレッドに関連づいていないthreadオブジェクトのget_id()を呼び出すと、デフォルトコンストラクトされたstd::thread::idが得られます。

std::thread t1([]{ /* ... */ });
assert(t1.get_id() != std::thread::id());
std::thread t2;
assert(t2.get_id() == std::thread::id());
// 出力もできる
std::cout << t1.get_id() << std::endl;

おまけ:スレッドの数はどれぐらいにすべき?

当たり前ですが、スレッドを増やせば処理がその分処理が早くなるわけではありません。ハードウェアのCPUのコア数や、コンテキストスイッチ、OSのリソース等、考えることはたくさんあります。
スレッドの数の指針としては、std::thread::hardware_concurrency()の値を参考にするといいと思います。もちろん、速度の向上以外にも、マルチスレッドにする理由はあるので(GUIとか)、ケースバイケースといってしまえばそうなってしまうのですが…。

*1:インターフェースとしては不十分なので、あくまで例と考えてください。