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

この記事は、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:インターフェースとしては不十分なので、あくまで例と考えてください。

例外安全について簡単にまとめた

https://github.com/Suikaba/publications/tree/master/exception-safety

例外安全について簡単にまとまっているものがあまり見受けられないので、作ってみた.
とはいったものの、僕はC++に疎いので、間違いがあるかもしれません.
その時は指摘をお願いします.

追記(2014/08/21).
heisseswasserさんのご指摘をうけ、資料の一部を修正しました.具体的には以下の部分を修正しました.

  • 2.12.3 の stack unwinding についての補足の部分で、gotoの記述に誤りがあったのを修正. また、頂いた情報を元に、longjmpについても加筆.

Boost.Serialization - BOOST_CLASS_VERSION

以下のコードを書いたとする.

namespace foo {

class bar {
    // ...
private:
    // serialize の実装
};

BOOST_CLASS_VERSION(bar, 1);
}

すると、以下の様なエラーが大量に出てくる.

error: 'foo::boost::**' has not been declared

どうやらこれはBOOST_CLASS_VERSIONをグローバル名前空間に書かないと起こるようである.
実装を読んでいないのではっきりとは言えないが、BOOST_CLASS_VERSIONを用いる際は名前空間の外にだそう.

namespace foo {
// ...
}
BOOST_CLASS_VERSION(foo::bar, 1);

using と using namespace

Twitterでこういうコードを見かけた。

#include <utility>
 
namespace N {
	using namespace std;
	class C {};
	void swap(C&, C&) {}
	void f() {
		int n1 = 1;
		int n2 = 2;
		swap(n1, n2); // compile Error
	}
}
 
int main() {
	N::f();
	return 0;
}

これはコンパイルエラーになる。だが、using namespace std;を using std::swapにすると正しく動く。それはなぜか。
using declaration(using std::swap)は現在のコンテキスト内にswapを提供する(この場合N内)が、using directive(using namespace std)はグローバルnamespace、すなわちglobal namespace(::)にstd名前空間内の変数やら関数が提供される。
コードに戻ろう。もし仮にusing namespace std;としていた場合を考える。swap(n1, n2)が呼ばれたタイミングでは、まずnamespace N内のswapを照会する。そして、今回それが見つかる。こうなるとここでLookupはおしまい、つまりglobal namespaceまでswapを見に行かないので、std::swapがそもそも候補に入ってない。結果、swap -> C& -> C& -> void が呼ばれてCompile Errorになる。
using std::swapとすれば、std::swapがN名前空間内に提供されるので、std::swapが正しく呼び出される。なのでコンパイルが通る。

確かこんな挙動だったと思いますが、間違ってたら誰か教えてください。

make_append_tuple を書いた

template の練習がてら書いてみようと思ったらウンコードが完成した。
tupleをくっつけるだけにしようと思っていたのだが、面白く無いのでめちゃくちゃにしてやった。
後悔はしていない。
まずはそれを見て貰いたいと思う。

#include <iostream>
#include <type_traits>
#include <tuple>

namespace mpl {

template <bool, typename T1, typename T2>
struct typeif_c
{
    using type = T1;
};

template <typename T1, typename T2>
struct typeif_c<false, T1, T2>
{
    using type = T2;
};

template <bool B, typename T1, typename T2>
struct eval_typeif_c
    : typeif_c<B, T1, T2>::type
{};


template <class M, typename T1, typename T2>
struct typeif
    : typeif_c<M::value, T1, T2>
{};

template <class M, typename T1, typename T2>
struct eval_typeif
    : typeif_c<M::value, T1, T2>::type
{};



template <typename> 
struct is_tuple : std::false_type {};
template <typename... Types>
struct is_tuple<std::tuple<Types...>> : std::true_type {};

template <typename...>
struct append_tuple_impl { using type = append_tuple_impl; };
template <typename T>
struct append_tuple_impl<T>
{
    using type = typename typeif_c<
                     is_tuple<T>::value,
                     T,
                     std::tuple<T>
                 >::type;
};
template <typename T, typename... Args>
struct append_tuple_impl<T, Args...>
    : append_tuple_impl<std::tuple<T>, Args...>
{};
template <typename... Types>
struct append_tuple_impl<std::tuple<Types...>, append_tuple_impl<>>
{
    using type = std::tuple<Types...>;
};
template <typename... Types1, typename... Types2>
struct append_tuple_impl<std::tuple<Types1...>, std::tuple<Types2...>>
{
    using type = std::tuple<Types1..., Types2...>; 
};
template <typename... Types1, typename T, typename... Types2>
struct append_tuple_impl<std::tuple<Types1...>, T, Types2...>
    : append_tuple_impl<std::tuple<Types1..., T>, typename append_tuple_impl<Types2...>::type>
{};
template <typename... Types1, typename... Types2, typename... Types3>
struct append_tuple_impl<std::tuple<Types1...>, std::tuple<Types2...>, Types3...>
    : append_tuple_impl<std::tuple<Types1..., Types2...>, typename append_tuple_impl<Types3...>::type>
{};


template <typename T>
using remove_r_t = typename std::remove_reference<T>::type;

template <typename... Types>
struct append_tuple
    : append_tuple_impl<remove_r_t<Types>...>
{};


template <
    index_t... N,
    typename... Types,
    typename T
    >
auto make_append_tuple_helper1( std::tuple<Types...>&& t, T&& a, index_tuple<N...> )
    -> decltype( std::make_tuple( std::get<N>( t )..., a ) )
{
    return std::make_tuple( std::get<N>( t )..., a );
}
template <
    index_t... N,
    typename... Types,
    typename T
    >
auto make_append_tuple_helper1( std::tuple<Types...>& t, T&& a, index_tuple<N...> )
    -> decltype( std::make_tuple( std::get<N>( t )..., a ) )
{
    return std::make_tuple( std::get<N>( t )..., a );
}
template <
    index_t... N,
    typename... Types,
    typename T
    >
auto make_append_tuple_helper2( T&& a, std::tuple<Types...>&& t, index_tuple<N...> )
    -> decltype( std::make_tuple( a, std::get<N>( t )... ) )
{
    return std::make_tuple( a, std::get<N>( t )... );
}
template <
    index_t... N,
    typename... Types,
    typename T
    >
auto make_append_tuple_helper2( T&& a, std::tuple<Types...>& t, index_tuple<N...> )
    -> decltype( std::make_tuple( a, std::get<N>( t )... ) )
{
    return std::make_tuple( a, std::get<N>( t )... );
}
template <
    typename... Types1,
    typename... Types2,
    index_t... N,
    index_t... M
    >
auto make_append_tuple_helper3( std::tuple<Types1...>&& t1, 
                                std::tuple<Types2...>&& t2, 
                                index_tuple<N...>,
                                index_tuple<M...> )
    -> decltype( std::make_tuple( std::get<N>( t1 )..., std::get<M>( t2 )... ) )
{
    return std::make_tuple( std::get<N>( t1 )..., std::get<M>( t2 )... );
}
template <
    typename... Types1,
    typename... Types2,
    index_t... N,
    index_t... M
    >
auto make_append_tuple_helper3( std::tuple<Types1...>& t1, 
                                std::tuple<Types2...>& t2, 
                                index_tuple<N...>,
                                index_tuple<M...> )
    -> decltype( std::make_tuple( std::get<N>( t1 )..., std::get<M>( t2 )... ) )
{
    return std::make_tuple( std::get<N>( t1 )..., std::get<M>( t2 )... );
}


template <
    typename T1,
    typename T2
    >
auto make_append_tuple_helper( T1&& t1, T2&& t2 )
    -> decltype( std::make_tuple( t1, t2 ) )
{
    return std::make_tuple( t1, t2 );
}

template <
    typename... Types,
    typename T
    >
std::tuple<Types..., remove_r_t<T>>
    make_append_tuple_helper( std::tuple<Types...>& t, T&& arg )
{
    return make_append_tuple_helper1( std::forward<std::tuple<Types...>>( t ),
                                      std::forward<T>( arg ),
                                      index_range<0, sizeof...(Types), 1>{} );
}
template <
    typename... Types,
    typename T
    >
std::tuple<Types..., remove_r_t<T>>
    make_append_tuple_helper( std::tuple<Types...>&& t, T&& arg )
{
    return make_append_tuple_helper1( std::forward<std::tuple<Types...>>( t ),
                                      std::forward<T>( arg ),
                                      index_range<0, sizeof...(Types), 1>{} );
}
template <
    typename T,
    typename... Types
    >
std::tuple<remove_r_t<T>, Types...>
    make_append_tuple_helper( T&& arg, std::tuple<Types...>& t )
{
    return make_append_tuple_helper2( std::forward<T>( arg ),
                                      std::forward<std::tuple<Types...>>( t ),
                                      index_range<0, sizeof...(Types), 1>{} );
}
template <
    typename T,
    typename... Types
    >
std::tuple<remove_r_t<T>, Types...>
    make_append_tuple_helper( T&& arg, std::tuple<Types...>&& t )
{
    return make_append_tuple_helper2( std::forward<T>( arg ),
                                      std::forward<std::tuple<Types...>>( t ),
                                      index_range<0, sizeof...(Types), 1>{} );
}

template <typename... Types1, typename... Types2>
std::tuple<Types1..., Types2...>
    make_append_tuple_helper( std::tuple<Types1...>&& t1, std::tuple<Types2...>&& t2 )
{
    return make_append_tuple_helper3( std::forward<std::tuple<Types1...>>( t1 ),
                                      std::forward<std::tuple<Types2...>>( t2 ),
                                      index_range<0, sizeof...(Types1), 1>{},
                                      index_range<0, sizeof...(Types2), 1>{} );
}
template <typename... Types1, typename... Types2>
std::tuple<Types1..., Types2...>
    make_append_tuple_helper( std::tuple<Types1...>& t1, std::tuple<Types2...>& t2 )
{
    return make_append_tuple_helper3( std::forward<std::tuple<Types1...>>( t1 ),
                                      std::forward<std::tuple<Types2...>>( t2 ),
                                      index_range<0, sizeof...(Types1), 1>{},
                                      index_range<0, sizeof...(Types2), 1>{} );
}
template <typename... Types1, typename... Types2>
std::tuple<Types1..., Types2...>
    make_append_tuple_helper( std::tuple<Types1...>& t1, std::tuple<Types2...>&& t2 )
{
    return make_append_tuple_helper3( std::forward<std::tuple<Types1...>>( t1 ),
                                      std::forward<std::tuple<Types2...>>( t2 ),
                                      index_range<0, sizeof...(Types1), 1>{},
                                      index_range<0, sizeof...(Types2), 1>{} );
}
template <typename... Types1, typename... Types2>
std::tuple<Types1..., Types2...>
    make_append_tuple_helper( std::tuple<Types1...>&& t1, std::tuple<Types2...>& t2 )
{
    return make_append_tuple_helper3( std::forward<std::tuple<Types1...>>( t1 ),
                                      std::forward<std::tuple<Types2...>>( t2 ),
                                      index_range<0, sizeof...(Types1), 1>{},
                                      index_range<0, sizeof...(Types2), 1>{} );
}


template <typename T>
auto make_append_tuple( T&& t )
    -> decltype( std::make_tuple( std::forward<T>( t ) ) )
{
    return std::make_tuple( std::forward<T>( t ) );
}
template <typename... Types>
std::tuple<Types...> make_append_tuple( std::tuple<Types...>& t )
{
    return t;
}
template <typename... Types>
auto make_append_tuple( std::tuple<Types...>&& t )
    -> decltype( std::move( t ) ) 
{
    return std::move( t );
}
template <typename T1, typename T2, typename... Args>
typename append_tuple<T1, T2, Args...>::type  
    make_append_tuple( T1&& t1, T2&& t2, Args&&... args )
{
    return make_append_tuple(
               make_append_tuple_helper(
                   std::forward<T1>( t1 ),
                   std::forward<T2>( t2 ) ),
               std::forward<Args>( args )... );
}

}


int main()
{
    auto tuple1 = std::make_tuple( 5, "hoge", true );
    
    // tuple<int, const char*, bool, float>
    auto tuple2 = mpl::make_append_tuple( tuple1, 1.5f );
    std::cout << std::get<0>( tuple2 ) << std::endl;
    
    // tuple<float, int, const char*, bool, float>
    auto tuple3 = mpl::make_append_tuple( 1.5f, tuple1 );
    std::cout << std::get<0>( tuple3 ) << std::endl;
    
    // tuple<int, const char*, bool, int, const char*, bool, float> 
    auto tuple4 = mpl::make_append_tuple( tuple1, tuple2 );
    std::cout << std::get<4>( tuple4 ) << std::endl;
    
    // tuple<int, int, int> なぜかmake_tupleみたいな使い方もできる
    auto tuple5 = mpl::make_append_tuple( 1, 2, 3 );
    std::cout << std::get<2>( tuple5 ) << std::endl;
    
    // tuple<bool, int, int, int, double>
    auto tuple6 = mpl::make_append_tuple( false, tuple5, 2.2 );
    std::cout << std::get<4>( tuple6 ) << std::endl;
}

(´・_・`)(´・_・`)(´・_・`)
なんというウンコード。計算量なんて目も当てられないレベルである。
(計算してみてあまりにもひどかったので言わない)。

気が向いた方はもっと効率よく実装してみてください。
僕はたとえ改善案が思いついたとしてもこのコードには触りたくないです。では

【追記】
@kester44 さんがもっとわかりやすい実装を書いてくださいました。

template <typename T>
struct is_tuple
    :    std::false_type
{};
 
 
template <typename ... Elements>
struct is_tuple<std::tuple<Elements ...> >
    :    std::true_type
{};
 
 
namespace detail
{


template <
    typename T,
    typename std::enable_if<is_tuple<typename std::decay<T>::type>::value>::type* = nullptr
    >
inline T wrap_tuple( T && x )
{
    return std::forward<T>( x );
}
 
 
template <
    typename T,
    typename std::enable_if<!is_tuple<typename std::decay<T>::type>::value>::type* = nullptr
    >
inline std::tuple<T> wrap_tuple( T && x )
{
    return std::make_tuple( std::forward<T>( x ));
}


}    // END: namespace (detail)
 
 
 
template <typename ... Elements>
inline auto make_append_tuple( Elements && ... elements )
    ->decltype( std::tuple_cat( detail::wrap_tuple( std::forward<Elements>( elements )) ... ))
{
    return std::tuple_cat( detail::wrap_tuple( std::forward<Elements>( elements )) ... );
}

めっちゃ簡潔ですね。俺のコードがいかにウンコだったかお分かりいただけたと思います。

std::tuple_cat の存在忘れてたよ…つらぽよ…もうだめだ…

GCC Bug 58046 - template operator= in SFINAE class

コンパイルエラーになって欲しかったのにICEになりやがったのでバグレポ。
SFINAE 使ってたらなぜかバグりました。でも少し変えたら動くんですよねー。
詳細はリンク先を参照ください。
バグレポの書き方とか教えてもらえばよかったかなぁ…

http://gcc.gnu.org/bugzilla/show_bug.cgi?id=58046

Boost.Log 追記モード・ユーザー定義の severity

Boost.Logはファイルに書き出せるわけですが、追記モードも可能です。
ついでに、自分で定義した severity を書きだす方法もどーぞ。

#include <boost/log/utility/setup/file.hpp>
#include <boost/log/utility/setup/common_attributes.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/attributes.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/support/date_time.hpp>

namespace logging = boost::log;
namespace attrs = boost::log::attributes;
namespace keywords = boost::log::keywords;
namespace sinks = boost::log::sinks;
namespace expr = boost::log::expressions;

enum severity_level
{
    debug,
    warning,
    error,
    critical
};
struct severity_tag;

std::ostream& operator<< ( std::ostream& strm, severity_level level )
{
    static const char* strs[] =
    {
        "debug",
        "warning",
        "error",
        "critical"
    };

    if ( static_cast< std::size_t >( level ) < sizeof( strs ) / sizeof( *strs ) )
        strm << strs[level];
    else
        strm << static_cast< int >( level );

    return strm;
}

int main()
{
    logging::register_simple_formatter_factory< severity_level, char >( "Severity" );
    logging::add_file_log(
        keywords::file_name = "logs/%Y%m%d.log",
        keywords::time_based_rotation = sinks::file::rotation_at_time_point( 0, 0, 0 ),
        keywords::format = 
        (
            expr::stream
                << expr::attr< unsigned int >( "LineID" ) << ":"
                << "[" << expr::format_date_time< boost::posix_time::ptime >( "TimeStamp", "%Y-%m-%d %H:%M:%S" )
                << "][" << expr::attr< severity_level, severity_tag >( "Severity" )
                << "]: " << expr::smessage
        ),
        // 又は、単に
        // keywords::format = "%LineID%:[%TimeStamp%][%Severity%]:%Message%"
        keywords::open_mode = (std::ios::out | std::ios::app) // std::ios のがそのまま使える
    );
    logging::sources::severity_logger<severity_level> slg;
    logging::add_common_attributes(); // LineID を使えるようにする
    logging::core::get()->add_global_attribute( "TimeStamp", attrs::local_clock() );
    BOOST_LOG_SEV( slg, debug ) << "hoge";
    BOOST_LOG_SEV( slg, error ) << "bar";
    BOOST_LOG_SEV( slg, warning ) << "piyo";
    BOOST_LOG_SEV( slg, critical ) << "foo";
}

ログファイルがこちら

1:[2013-08-02 19:15:24][debug]: hoge
2:[2013-08-02 19:15:24][error]: bar
3:[2013-08-02 19:15:24][warning]: piyo
4:[2013-08-02 19:15:24][critical]: foo

severity に関しては他にも方法あるかも。多分これが一番楽だと思います。
Boost.Logのリファレンス適当に読んだだけなので問題があったら言ってください。