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

std::threadをあとから開始。それとムーブ対応したコンテナについて

C++

std::thread/boost::threadは、コンストラクタでスレッドの実行を開始するので、あとから実行しようと思ったらどうすればいいのか。ムーブセマンティクスがなかった頃、Boost.Threadではshared_ptrと組み合わせることでこれを実現していました。

#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>

class X {
    boost::shared_ptr<boost::thread> thread_;

    void do_() { std::cout << "do" << std::endl; }

public:
    ~X()
    {
        if (thread_)
            thread_->join();
    }

    // あとで実行(shared_ptr::resetでthreadを作る)
    void start()
    {
        thread_.reset(new boost::thread([this] { do_(); }));
    }
};

int main()
{
    X x;
    x.start();
}

ムーブセマンティクスに対応してからは、std::thread/boost::threadは、以下のようにして「あとから実行」を簡単にできるようになりました:

  • thread型オブジェクトをデフォルトコンストラクトする
  • あとから実行する際に、実行する関数をコンストラクタに指定したthread型の一時オブジェクトを作り、ムーブ代入する

これで、開始したスレッドの所有権を、デフォルトコンストラクタしたいわば空のthreadオブジェクトに移譲することができ、それによってスレッドをあとから実行することができます。

#include <iostream>
#include <vector>
#include <thread>

class X {
    std::thread thread_; // デフォルトコンストラクト

    void do_() { std::cout << "do" << std::endl; }

public:
    ~X()
    {
        if (thread_.joinable())
            thread_.join();
    }

    void start()
    {
        // 開始したスレッドを持つthread型一時オブジェクトをムーブ代入
        thread_ = std::thread([this] { do_(); });

        // もしくはthreadオブジェクトを作ってから明示的にmove
        // std::thread t([this] { do_(); });
        // thread_ = std::move(t);
    }
};

int main()
{
    X x;
    x.start();
}

Boost.Threadは、Boost.Moveが入るだいぶ前から自前のムーブ機構を持ってるので、このイディオムを使用することができます:
参照: boost::threadはC++03/C++0xの両方でmove対応してる


追記 {
ムーブ使わなくても昔からswapができました。
}


さらに、C++03までの標準コンテナは要素型に対して「コピー可能であること」を要求していたのに対し、C++11の標準コンテナは「コピー可能またはムーブ可能であること」という要求になったため、コピー不可なthreadクラスのオブジェクトもコンテナに持つことができます。

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <string>
#include <algorithm>

std::mutex mutex_;
void print(const std::string& s)
{
    (std::lock_guard<std::mutex>(mutex_)), (std::cout << s << std::endl);
}

void f() { print("f"); }
void g() { print("g"); }

int main()
{
    std::vector<std::thread> threads;

    threads.emplace_back(f);
    threads.emplace_back(g);

    // もしくはthreadオブジェクトを作ってムーブ
//  threads.push_back(std::thread(f));
//  threads.push_back(std::thread(g));

    std::for_each(threads.begin(), threads.end(), [](std::thread& t) { t.join(); });
}

Boost.Threadには、ムーブ対応してなかった頃のためか、std::listベースで作られたboost::thread_groupというクラスがありますが、ムーブ対応したことでこのクラスは不要になったかもしれません。
Boostで「thread_groupはまだ必要?」という議論が行われてる最中です。

Do we need thread_group now?


ちなみに、標準コンテナの「コピー不可だけどムーブは可能」という型を受け入れるという仕様は、スレッドだけじゃなく多くの場面で役立ちます。UIを作る場合、ボタンやウィンドウなどのクラスは多くの場合コピー不可で設計されています。他にも、通信で使用するソケットクラスや、シリアライザーのようなファイルリソースを内部に包含するクラスもコピー不可である場合があります。これらをコンテナに持つ際に、C++11のコンテナに対するムーブ拡張は非常に役立ちます。