動機
C++11でカジュアルにスレッドが作れるようになったのに、そのバリア同期手段はboost::barrierかpthread_barrierぐらいしか使えなくてもっと依存のないポータブルな実装が欲しい事があったので書き捨てたものを公開。
condition_variable
とnotify_all
を使った物が一般的だけれど、短時間で同期できることがわかっている場合にはスピンによる待機が一番パフォーマンスが出るのでスピンにした。
実装
The Art of Multiprocessor Programmingのセンス反転バリアをそのまま実装した。
スレッドローカルストレージをクラスごとにポータブルに作る方法が分からなかったし、そもそもスレッドは開始時にTIDを渡しているのでそれをwait()
の引数に渡すようにした。
やりたい事はbooleanの反転だったけれどstd::vector<bool>
は触ってはいけない(特にマルチスレッド環境にはあまり良くないテンプレート特殊化がされている)のでstd::vector<int>
を利用した。
随所で <<6
とビットシフトしているのは、個々のスレッドの状態を保持する場所同士がキャッシュラインの都合上64byteずつ離れていて欲しかったから。
# include <atomic>
# include <vector>
class spin_barrier {
spin_barrier() = delete;
spin_barrier(const spin_barrier &) = delete;
spin_barrier& operator=(const spin_barrier &) = delete;
public:
spin_barrier(size_t threads) :
threads_(static_cast<int>(threads)),
waits_(static_cast<int>(threads)),
sense_(false),
my_sense_(threads << 6) {
for (size_t i = 0; i < threads; ++i) {
my_sense_[i << 6] = true;
}
}
void wait(int i) {
int sense = my_sense_[i << 6];
if (waits_.fetch_sub(1) == 1) {
waits_.store(threads_);
sense_.store(sense != 0, std::memory_order_release);
} else {
while (sense != sense_);
}
my_sense_[i << 6] = !sense;
}
private:
const int threads_;
std::atomic<int> waits_;
std::atomic<bool> sense_;
std::vector<int> my_sense_;
};
使い方
10スレッドが100回バリア同期するだけのコード。
コンパイル時には -std=c++11
と -pthread
オプションを忘れずに。
# include <iostream>
# include <thread>
# include <vector>
// ここに上のbarrierクラスを書く
int main() {
const int n_workers = 10;
spin_barrier barrier(n_workers); // shared
std::vector<std::thread> workers;
workers.reserve(n_workers);
for (int tid = 0; tid < n_workers; ++tid) {
workers.emplace_back([&, tid] {
for (int i = 0; i < 100; ++i) {
barrier.wait(tid);
std::cerr << "[" << tid << "]: "
<< i << "th barrier ok." << std::endl;
}
});
}
for (auto& worker : workers) {
worker.join();
}
}
簡単ですね。