マルチスレッドサポート
概要
C++11ではスレッド関連の機能が言語機能、標準ライブラリで提供されるようになった。 スレッド機能をサポートするにあたり、言語機能ではMulti-threaded executions and data race(マルチスレッドにおける実行とデータ競合) に関する規定が追加されている。 ここではプログラムの実行順に関する仕様と、データ競合が発生する条件が記載されている。 プログラムの実行順に関する説明は複雑であるため、本書では省略し、データ競合が発生する条件についてを説明する。 そして、スレッドサポートにより新たに追加された thread_local キーワードについて説明する。
データ競合条件
C++11では、データ競合を引き起こす動作が明確に規定された。あるプログラムが、以下の条件を満たす操作を実行する場合、そのプログラムはデータ競合を持つ。
- あるオブジェクト(スカラ型)に対して同時に発生する2つの操作の内、どちらか一方が書き込み動作である。
※ スカラ型とは、基礎型、列挙型、ポインタ型の総称である
そして、データ競合を持つプログラムの動作は Undefined-behavior(未定義の振る舞い) となる。よって正しいプログラムとはデータ競合を持たないプログラムである。 この条件から分かる通り、データ競合は次のような条件を満たすことで回避可能である。
- 別のオブジェクトに対する操作である
- 同じオブジェクトへの操作が常にシーケンシャル (同時に発生しない)
- 2つの操作が共に読み込み操作のみである
言語仕様として規定されたデータ競合条件については上記の通りであるが、標準ライブラリについてもデータ競合に関する規定がある。 但し、こちらはライブラリ実装者に対しての制約であり、どのような実装でなければならないかを規定している。 そこで、ここではその制約から考えられる、標準ライブラリを使用する上でのデータ競合の考え方を記載する。
- 関数の引数が非constのポインタまたは参照の場合、関数内部でその引数から直接的または間接的にアクセス可能なオブジェクトに書き込み操作が発生する可能性がある。
- 上記条件に加え、メンバ関数が const 指定の場合は、メンバ変数に対して読み込み操作に限定される。(非constの場合は書き込み操作が発生する可能性がある)
基本的には上記のルールに則って各ライブラリのデータ競合が定義される。ただし、特別にその関数に対してデータ競合規定が設けられている場合、その規定で動作する。 上記の方針から、例えばライブラリの場合、以下に示した関数(const指定有り版)の呼び出しはいかなる操作であってもデータ競合は発生しない。(純粋に関数呼び出しのみを行う場合)
- begin, end, rbegin, rend, front, back, data, find, lower_bound, upper_bound, equal_range, at, operator[]
※ operator[]は、連想コンテナ、非順序連想コンテナを除く
thread_localキーワード
スレッドサポートに伴い、C++11では、変数の生存期間を指定するthread_localが追加された。 ブロックスコープ、ネームスペースの変数、及びstaticメンバ変数の宣言時に指定可能であり、このキーワードを指定された変数はスレッド単位の生存期間を持つ。 ブロックスコープで宣言された場合、暗黙的にstaticとして扱われる。 thread_localキーワードを指定して宣言された変数名を使用する場合、動作しているスレッドに関連した変数が参照される。
#include <thread>
#include <mutex>
#include <iostream>
thread_local int a = 0;
std::mutex m;
void f() {
thread_local int x = 0; //OK スレッド毎に生成される変数
std::lock_guard<std::mutex> lock(m);
std::cout << "&x = " << &x << std::endl; //スレッド毎に異なるアドレスが表示される (再利用されない限り)
std::cout << "&a = " << &a << std::endl; //スレッド毎に異なるアドレスが表示される (再利用されない限り)
}
int main() {
std::thread t1(f);
std::thread t2(f);
t1.join()
t2.join()
}