C++11のstd::threadを使うと、関数を別スレッドで実行できます。ただしスレッドを増やせば必ず速くなるわけではなく、寿命、共有データの同期、例外、終了処理の設計が必要です。本記事では作成・join・detachから、条件変数、スレッドプール、並列集計、Boost.Asio、Qtの応用まで、動くコードと注意点を順に解説します。
std::threadの基本
C++11で導入されたstd::threadは、標準ライブラリに含まれるクラスで、スレッドを簡単に作成することができます。以下では、基本的なstd::threadの使用方法について説明します。
ビルド条件とサンプルの分け方
基本例とプール・キューはC++11、Webのmove captureはC++14以降、Qt例はC++17設定、後述のjthreadはC++20です。mainを含む例は別々のファイルでビルドします。ヘッダーとして示したファイルは使用例と同じディレクトリに保存してください。LinuxのGCCでは、たとえば次のようにコンパイルとリンクに-pthreadを指定します。
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread main.cpp -o exampleオプションはGCCの公式資料を参照してください。MSVCなどではプロジェクト側で対応するC++言語標準を選びます。
スレッドの作成
スレッドを作成するには、std::threadオブジェクトを初期化し、実行したい関数やラムダ式を渡します。以下に例を示します。
#include <iostream>
#include <thread>
void print_message() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(print_message);
t.join(); // スレッドの終了を待機
return 0;
}この例では、print_message関数を新しいスレッドで実行し、メインスレッドでその終了を待機しています。
ラムダ式を使ったスレッド作成
関数だけでなく、ラムダ式を使ってスレッドを作成することもできます。以下に例を示します。
#include <iostream>
#include <thread>
int main() {
std::thread t([] {
std::cout << "Hello from lambda thread!" << std::endl;
});
t.join(); // スレッドの終了を待機
return 0;
}ラムダ式を使うことで、より簡潔にスレッドを作成できます。
引数付きスレッド
スレッドに引数を渡すこともできます。以下の例では、引数付き関数をスレッドで実行します。
#include <iostream>
#include <thread>
void print_number(int n) {
std::cout << "Number: " << n << std::endl;
}
int main() {
std::thread t(print_number, 42);
t.join(); // スレッドの終了を待機
return 0;
}この例では、print_number関数に引数42を渡してスレッドを作成しています。
これらの基本的な使い方を理解することで、C++でのスレッドプログラミングの第一歩を踏み出すことができます。次のセクションでは、スレッドの結合と分離について詳しく説明します。
引数は通常コピー/ムーブ、参照は明示する
スレッドへ渡す引数は通常コピーまたはムーブされます。元のオブジェクトへの参照を渡すにはstd::ref、読み取り専用参照にはstd::crefを使い、参照先を終了まで生存させます。ラムダの参照キャプチャも同じです。参照を渡しただけでは共有データは同期されません。
#include <functional>
#include <iostream>
#include <thread>
void increment(int& value) { ++value; }
int main() {
int value = 0;
std::thread worker(increment, std::ref(value));
worker.join(); // valueが生きているうちに待つ
std::cout << value << '\n'; // 1
}スレッドの結合と分離
スレッドを作成した後、その終了を適切に処理するために、スレッドの結合(join)と分離(detach)について理解することが重要です。これらの操作は、スレッドのライフサイクル管理に欠かせません。
スレッドの結合 (join)
join()を呼んだスレッドは、対象の実行が終了するまで待機します。呼び出し元はメインスレッドに限りません。正常に戻った後は対象スレッドの完了と同期するため、そのスレッドが書いた結果を読み取れます。
#include <iostream>
#include <thread>
void print_message() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(print_message);
t.join(); // スレッドの終了を待機
std::cout << "Thread has finished." << std::endl;
return 0;
}この例では、t.join()が呼び出されることで、print_message関数が終了するまでメインスレッドが待機します。スレッドが終了すると、「Thread has finished.」というメッセージが表示されます。
joinable・例外・RAIIによる寿命管理
joinable()は「まだ実行中か」ではなく、threadが実行スレッドを表しているかを示します。join/detach済みへの再呼び出しや、自分自身へのjoinは不正です。さらにワーカーから抜けた未捕捉例外は、呼び出し元のtry/catchでは捕まらずterminateになります。ワーカー内で捕捉し、exception_ptrやfutureで伝えます。公開作業草案のthread仕様を参照してください。
例外経路でもjoinを忘れないよう、後の例で使うガードをthread_joiner.hに保存します。threadの直後にガードを作り、ガードより先にthreadやワーカーが参照する変数を破棄しないことが前提です。ガードの破棄時には待機するため、ワーカーを終了可能にする処理も必要です。
#ifndef THREAD_JOINER_H
#define THREAD_JOINER_H
#include <thread>
// このガードとthreadは作成元のスレッドだけで操作する。
class ThreadJoiner {
public:
explicit ThreadJoiner(std::thread& t) noexcept : thread_(t) {}
~ThreadJoiner() {
if (thread_.joinable()) thread_.join();
}
ThreadJoiner(const ThreadJoiner&) = delete;
ThreadJoiner& operator=(const ThreadJoiner&) = delete;
private:
std::thread& thread_;
};
#endifスレッドの分離 (detach)
detach()はスレッドの実行とstd::threadオブジェクトの関連を切り離します。後から同じオブジェクトでjoin()はできませんが、独立したプロセスになるわけではなく、プロセス終了後の継続も保証されません。通常はjoinによる寿命管理を優先します。以下は仕組みを示す例として、別のfutureで終了を待ちます。
#include <exception>
#include <future>
#include <iostream>
#include <thread>
#include <utility>
int main() {
std::promise<void> done;
auto completed = done.get_future();
std::thread t([](std::promise<void> signal) {
try {
std::cout << "Hello from detached thread!\n";
signal.set_value_at_thread_exit();
} catch (...) {
signal.set_exception_at_thread_exit(std::current_exception());
}
}, std::move(done));
t.detach();
completed.get(); // sleepではなく、終了通知と例外を受け取る
}sleep_forで長めに眠っても処理完了の保証にはなりません。この例ではset_value_at_thread_exit()により終了時にfutureを準備完了にし、get()で待ちます。処理中の例外もfutureに渡します。単に終了を待つだけなら、最初からjoin()を使う方が簡潔です。C++公開作業草案のpromise仕様も参照してください。
注意点
joinable()なstd::threadのデストラクタはstd::terminate()を呼びます。プログラム終了時だけでなく、途中のスコープ終了や例外による巻き戻しでも発生します。実行が終了したスレッドも、join/detach前ならjoinableです。デフォルト構築・移動後の空のオブジェクトなどにはjoin/detachしません。- detach後もfutureなど別の仕組みで完了通知はできます。ただし参照先や共有オブジェクトの寿命を別途保証する必要があり、終了済みのローカル変数や破棄済みオブジェクトへアクセスしてはいけません。
次のセクションでは、スレッドの同期について説明します。スレッド間でデータを安全にやり取りする方法を学びましょう。
スレッドの同期
同じメモリ位置へのアクセスが競合し、少なくとも一方が書き込みで、適切な同期がなければデータ競合になり得ます。C++のデータ競合は未定義動作です。単に「たまに結果がずれる」問題ではありません。読み取り専用データの共有まで禁止されるわけではなく、mutexやatomicなどで必要な同期を設計します。公開作業草案のデータ競合規則を参照してください。
mutexを使った同期
std::mutexは、排他制御のための基本的な同期プリミティブです。複数のスレッドが同時に同じデータにアクセスするのを防ぎます。
#include <iostream>
#include <thread>
#include <mutex>
#include <string>
#include "thread_joiner.h"
std::mutex mtx;
void print_message(const std::string& message) {
mtx.lock(); // クリティカルセクションの開始
std::cout << message << std::endl;
mtx.unlock(); // クリティカルセクションの終了
}
int main() {
std::thread t1(print_message, "Hello from thread 1!");
ThreadJoiner guard1(t1);
std::thread t2(print_message, "Hello from thread 2!");
ThreadJoiner guard2(t2);
t1.join();
t2.join();
return 0;
}これは手動lock/unlockの動作を示す例です。ロック中の処理が例外を投げるとunlockに到達しないため、実用コードでは次のlock_guardやunique_lockを使います。出力順は決まりませんが、同じmutexを使う出力同士の混在を防げます。
lock_guardを使った同期
std::lock_guardは、RAII(Resource Acquisition Is Initialization)パターンを使用して、自動的にmutexをロックおよびアンロックします。これにより、mutexのロック解除を忘れることを防ぎます。
#include <iostream>
#include <thread>
#include <mutex>
#include <string>
#include "thread_joiner.h"
std::mutex mtx;
void print_message(const std::string& message) {
std::lock_guard<std::mutex> lock(mtx); // lock_guardがスコープ内でmutexをロック
std::cout << message << std::endl;
// スコープを抜けるときに自動的にアンロックされる
}
int main() {
std::thread t1(print_message, "Hello from thread 1!");
ThreadJoiner guard1(t1);
std::thread t2(print_message, "Hello from thread 2!");
ThreadJoiner guard2(t2);
t1.join();
t2.join();
return 0;
}この例では、std::lock_guardを使用してmutexをロックおよびアンロックしています。lock_guardはスコープを抜けるときに自動的にmutexをアンロックするため、mtx.unlock()を明示的に呼び出す必要はありません。
unique_lockを使った柔軟な同期
std::unique_lockは、std::lock_guardよりも柔軟な同期手段を提供します。条件変数と組み合わせて使用する場合に便利です。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include "thread_joiner.h"
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_message() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // readyがtrueになるまで待機
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(print_message);
ThreadJoiner guard(t);
// 準備ができたら通知
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 待機しているスレッドに通知
t.join();
return 0;
}条件変数の待機中はmutexが解放され、戻る前に再取得されます。readyは読み書きの両方を同じmutexで保護します。述語付きのwaitは、通知のない起床(spurious wakeup)や待機前に状態が更新された場合にも対応します。通知だけを完了状態として扱わないことが重要です。
次のセクションでは、条件変数の詳細と利用方法について解説します。スレッド間でのより高度な同期手法を学びましょう。
条件変数の利用
条件変数(condition_variable)は、スレッド間の待機と通知のメカニズムを提供し、スレッドの効率的な同期を実現します。条件変数を使うことで、スレッドが特定の条件が満たされるまで待機し、条件が満たされたときに通知を受けて実行を再開できます。
条件変数の基本的な使い方
条件変数を使用するには、std::condition_variableとstd::unique_lockを組み合わせます。以下の例では、条件変数を使って、メインスレッドからスレッドへの通知を行います。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include "thread_joiner.h"
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_message() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // readyがtrueになるまで待機
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(print_message);
ThreadJoiner guard(t);
// 準備ができたら通知
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 待機しているスレッドに通知
t.join();
return 0;
}この例では、print_messageスレッドがready変数がtrueになるまで待機し、メインスレッドがreadyをtrueに設定してcv.notify_one()で通知しています。
複数スレッドの待機と通知
条件変数は複数のスレッドに対して待機と通知を行うこともできます。以下の例では、複数のスレッドが同じ条件変数を使って待機し、メインスレッドが一度に全てのスレッドに通知します。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_message(int id) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // readyがtrueになるまで待機
std::cout << "Hello from thread " << id << "!" << std::endl;
}
int main() {
std::vector<std::thread> threads;
threads.reserve(5);
try {
for (int i = 0; i < 5; ++i) {
threads.emplace_back(print_message, i);
}
} catch (...) {
{ std::lock_guard<std::mutex> lock(mtx); ready = true; }
cv.notify_all(); // 作成済みの待機スレッドを終了可能にする
for (auto& t : threads) t.join();
throw;
}
// 準備ができたら全てのスレッドに通知
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_all(); // 全ての待機しているスレッドに通知
for (auto& t : threads) {
t.join();
}
return 0;
}この例では、5つのスレッドがそれぞれprint_message関数を実行し、readyがtrueになるまで待機します。メインスレッドがreadyをtrueに設定し、cv.notify_all()で全てのスレッドに通知します。
条件変数のタイムアウト付き待機
条件変数のwait_forメソッドを使うと、特定の時間だけ待機し、その後にタイムアウトとして処理を続行できます。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include "thread_joiner.h"
#include <chrono>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_message() {
std::unique_lock<std::mutex> lock(mtx);
if(cv.wait_for(lock, std::chrono::seconds(2), []{ return ready; })) {
std::cout << "Hello from thread!" << std::endl;
} else {
std::cout << "Timeout reached, thread continues." << std::endl;
}
}
int main() {
std::thread t(print_message);
ThreadJoiner guard(t);
// 1秒後に通知
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 待機しているスレッドに通知
t.join();
return 0;
}この例では2秒を待機時間の目安にしますが、スケジューリングやmutex再取得で実際に戻る時刻が遅くなることがあります。戻り値は述語がtrueになったかを表します。時間内の完了を厳密に保証するリアルタイム処理ではありません。条件変数の仕様も参照してください。
次のセクションでは、効率的なスレッド管理のためのスレッドプールの実装方法について解説します。
スレッドプールの実装
スレッドプールは、複数のスレッドを効率的に管理し、タスクを並行して実行するための設計パターンです。これにより、スレッドの作成や破棄のオーバーヘッドを削減し、システムリソースを最適化できます。ここでは、基本的なスレッドプールの実装方法を説明します。
スレッドプールの基本構造
スレッドプールは、以下の主要コンポーネントで構成されます。
- スレッドのコンテナ
- タスクキュー
- タスクの追加とスレッドへの分配
- スレッドの実行ループ
基本的なスレッドプールの実装例
以下はthread_pool.hとして保存する学習用実装です。C++11から使えるpackaged_task<void()>をキューに入れ、タスクの完了と例外をfutureで受け取ります。スレッド数0を拒否し、初期化途中のスレッド作成失敗も回収します。
#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <condition_variable>
#include <cstddef>
#include <future>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <thread>
#include <utility>
#include <vector>
class ThreadPool {
public:
explicit ThreadPool(std::size_t count) {
if (count == 0) throw std::invalid_argument("thread count is zero");
workers_.reserve(count);
try {
for (std::size_t i = 0; i < count; ++i) {
workers_.emplace_back([this] { worker_loop(); });
}
} catch (...) {
finish(); // 途中まで作成したスレッドも回収する
throw;
}
}
~ThreadPool() { finish(); }
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
template<class F>
std::future<void> enqueue_task(F&& fn) {
std::packaged_task<void()> task(std::forward<F>(fn));
auto result = task.get_future();
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopping_) throw std::runtime_error("pool is stopping");
tasks_.push(std::move(task));
}
condition_.notify_one();
return result;
}
private:
void finish() {
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
condition_.notify_all();
for (auto& worker : workers_) {
if (worker.joinable()) worker.join();
}
}
void worker_loop() {
for (;;) {
std::packaged_task<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] {
return stopping_ || !tasks_.empty();
});
if (stopping_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task(); // ユーザー処理の例外はfutureへ保存される
}
}
std::vector<std::thread> workers_;
std::queue<std::packaged_task<void()>> tasks_;
std::mutex mutex_;
std::condition_variable condition_;
bool stopping_ = false;
};
#endifスレッドプールの利用例
次に、このスレッドプールを利用してタスクを並列に実行する方法を示します。
#include "thread_pool.h"
#include <chrono>
#include <iostream>
int main() {
std::mutex output_mutex;
ThreadPool pool(4);
std::vector<std::future<void>> results;
for (int i = 0; i < 10; ++i) {
results.push_back(pool.enqueue_task([i, &output_mutex] {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lock(output_mutex);
std::cout << "Task " << i << " executed.\n";
}));
}
for (auto& result : results) result.get(); // 完了待ちと例外の受け取り
} // poolのデストラクタは残りのタスクを処理してから全workerをjoin10個のタスクを4つのworkerで処理します。短いsleepは模擬的な作業であり、全体の完了判定には使いません。各future::get()が完了を待ち、タスクの例外があれば呼び出し側で再送出します。デストラクタはキューを排出してjoinします。packaged_taskの仕様を参照してください。
スレッドプールの応用と拡張
この実装のキューは無制限で、タイムアウトやキャンセル、結果型の汎用化はありません。投入速度が処理速度を上回る用途では、キュー上限や受付制御が必要です。プールの破棄は所有側のスレッドで行い、破棄と投入を競合させたり、worker自身がプールを破棄したりしないでください。同じプール内で別タスクの完了を待つ設計も、workerを使い切ると停止します。拡張候補は次のとおりです。
- 動的なスレッド数の調整: ワークロードに応じてスレッドの数を動的に調整する機能を追加できます。
- 優先度付きタスクキュー: タスクに優先度を設定し、高優先度のタスクを先に処理するように変更できます。
- タスクのキャンセル: 実行中または待機中のタスクをキャンセルする機能を追加できます。
次のセクションでは、スレッドを使った並列計算の実践例について解説します。これにより、実際のアプリケーションでのスレッドの使用方法を具体的に学びます。
実践例: 並列計算
独立した仕事を分割できる場合、複数のスレッドで並列に計算できます。ただし作成・同期のコストやメモリ帯域があるため、速度向上は実測が必要です。ここでは正しい結果の集約を先に確認します。
例: 配列の要素の合計を計算する
以下に、配列の要素を並列で合計する例を示します。この例では、複数のスレッドを使用して配列の部分ごとの合計を計算し、最終的にそれらの合計を集計します。
#include <cstdint>
#include <exception>
#include <iostream>
#include <numeric>
#include <thread>
#include <vector>
int main() {
const std::vector<int> numbers(1000000, 1);
const std::size_t count = 4;
std::vector<std::int64_t> results(count, 0);
std::vector<std::thread> threads;
threads.reserve(count);
try {
for (std::size_t i = 0; i < count; ++i) {
const auto start = numbers.size() * i / count;
const auto end = numbers.size() * (i + 1) / count;
threads.emplace_back([&, i, start, end] {
results[i] = std::accumulate(numbers.begin() + start,
numbers.begin() + end, std::int64_t{0});
});
}
} catch (...) {
for (auto& thread : threads) thread.join();
throw;
}
for (auto& thread : threads) thread.join();
const auto total = std::accumulate(results.begin(), results.end(),
std::int64_t{0});
std::cout << "Total sum: " << total << '\n'; // 1000000
}この例では100万個の1を4分割し、各スレッドが異なるresults[i]に書き込みます。入力とresultsのサイズは実行中に変えず、join後に集計するため、同じ要素への競合を避けられます。累積の初期値を64ビット整数にして、途中の計算もその型で行います。一般の入力では64ビットの範囲も超えないか検討が必要です。
効率の向上
逐次版と並列版を同じ最適化設定・同じ入力で比較してください。小さい入力や単純な加算では、スレッド作成やメモリアクセスの負担で並列版が遅くなる場合があります。繰り返し処理ではプールによる再利用や、十分な粒度の分割も検討します。
スレッド数の動的調整
スレッド数を動的に調整することで、さらに効率を向上させることができます。スレッド数はシステムのCPUコア数に基づいて設定するのが一般的です。
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <thread>
int main() {
const unsigned hint = std::thread::hardware_concurrency();
const std::size_t tasks = 4; // 実際の分割可能な仕事の数
const std::size_t workers = std::min<std::size_t>(hint ? hint : 1, tasks);
std::cout << "Worker candidate: " << workers << '\n';
}hardware_concurrency()はハードウェア並行実行数のヒントであり、作成できる最大スレッド数ではありません。取得できなければ0を返すので、ゼロ除算やworker数0を避けるフォールバックが必要です。コンテナのCPU割当、仕事数、メモリ、I/O待ちも考慮して実測します。
次のセクションでは、スレッドを使ったWebサーバーの実装例を紹介します。ネットワークプログラミングにおけるスレッドの実用的な利用方法を学びましょう。
実践例: Webサーバー
スレッドを使用することで、複数のクライアントからのリクエストを同時に処理できるWebサーバーを構築できます。ここでは、std::threadを使って簡易Webサーバーを実装する方法を紹介します。
簡易Webサーバーの実装
以下はBoost.Asioを使うローカル学習用のHTTP応答例です。ソケットをラムダへ移動するためC++14以降が必要です。外部公開しないよう127.0.0.1に限定し、受け付け総数も制限します。まず共通処理をhttp_example.hへ保存してください。
#ifndef HTTP_EXAMPLE_H
#define HTTP_EXAMPLE_H
#include <boost/asio.hpp>
#include <istream>
#include <stdexcept>
#include <string>
using boost::asio::ip::tcp;
inline void handle_client(tcp::socket socket) {
boost::asio::streambuf request(4096); // ヘッダー用バッファの上限
boost::asio::read_until(socket, request, "\r\n\r\n");
std::istream input(&request);
std::string method, target, version;
input >> method >> target >> version;
const bool ok = method == "GET" && target == "/" && version == "HTTP/1.1";
const std::string body = ok ? "Hello, World!" : "Not Found";
const std::string response = std::string("HTTP/1.1 ") +
(ok ? "200 OK" : "404 Not Found") +
"\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: " +
std::to_string(body.size()) + "\r\nConnection: close\r\n\r\n" + body;
boost::asio::write(socket, boost::asio::buffer(response));
}
#endif次を別ファイルserver_threads.cppとして保存します。各接続をstd::threadで処理し、全スレッドをjoinしてからio_contextを破棄します。4接続に達するまではacceptで待機します。
#include "http_example.h"
#include <exception>
#include <iostream>
#include <thread>
#include <utility>
#include <vector>
int main() {
boost::asio::io_context io;
tcp::acceptor acceptor(io, tcp::endpoint(
boost::asio::ip::address_v4::loopback(), 8080));
std::vector<std::thread> clients;
std::vector<std::exception_ptr> errors(4);
clients.reserve(4);
try {
// 学習用:4接続を受け付けたら受け付けを終了する
for (std::size_t i = 0; i < 4; ++i) {
tcp::socket socket(io);
acceptor.accept(socket);
clients.emplace_back([socket = std::move(socket), &errors, i]() mutable {
try { handle_client(std::move(socket)); }
catch (...) { errors[i] = std::current_exception(); }
});
}
} catch (...) {
for (auto& client : clients) client.join();
throw;
}
for (auto& client : clients) client.join();
for (const auto& error : errors) {
if (error) std::rethrow_exception(error);
}
}共通処理はヘッダー終端まで同期的に読み、GET /への固定応答を返します。ヘッダーバッファには上限を設けていますが、完全なHTTPパーサーではなく、読み書きの期限もありません。遅いクライアントで停止し得るため、公開サーバーにはそのまま使えません。Boost.Asioのread_until仕様を参照してください。
詳細な解説
- Boost.Asioの利用: Boost.Asioは、C++で非同期ネットワークプログラミングを行うためのライブラリです。このライブラリを使うことで、シンプルなコードで効率的なネットワークプログラムを実装できます。
- 非同期I/O: この例では、acceptor.accept(socket)が同期的にクライアント接続を待機しています。非同期I/Oを使用することで、さらなるパフォーマンス向上が可能です。
スレッド管理の改善
接続ごとにスレッドを作る方式は、無制限に拡張すると資源不足になります。次は既出のプールでworker数を固定する例です。実運用にはさらに接続・待ち行列の上限、I/O期限、正常停止、入力検証が必要で、プールだけでは解決しません。
例: スレッドプールを使用したWebサーバー
http_example.hとthread_pool.hを同じディレクトリに置き、次をserver_pool.cppとして保存します。8接続を受け付けてから完了を待ちます。同期I/Oのため、各workerが読み書き待ちで占有される点は変わりません。
#include "http_example.h"
#include "thread_pool.h"
#include <utility>
int main() {
boost::asio::io_context io; // poolより長く生存させる
tcp::acceptor acceptor(io, tcp::endpoint(
boost::asio::ip::address_v4::loopback(), 8080));
ThreadPool pool(4);
std::vector<std::future<void>> results;
// 学習用に受け付け総数も8接続で制限する
for (int i = 0; i < 8; ++i) {
tcp::socket socket(io);
acceptor.accept(socket);
results.push_back(pool.enqueue_task(
[socket = std::move(socket)]() mutable {
handle_client(std::move(socket));
}));
}
for (auto& result : results) result.get();
}tcp::socketをmove captureしたラムダはコピーできないため、そのままstd::function<void()>へ格納する設計ではコンパイルできません。ここでは移動可能なpackaged_taskキューに渡します。std::functionのコピー可能性要件と区別してください。Boost.Asioには既成のthread_poolもあります。これらのBoost例は依存ライブラリのある環境で別途ビルド・通信検証が必要です。
次のセクションでは、スレッド安全なデータ構造の設計と実装について説明します。複数のスレッドが同時にアクセスするデータを安全に扱う方法を学びましょう。
スレッド安全なデータ構造
複数のスレッドが同時にデータにアクセスする際に、データの整合性を保つことは非常に重要です。スレッド安全なデータ構造を使用することで、データ競合を防ぎ、プログラムの正確性と安定性を確保できます。ここでは、スレッド安全なデータ構造の設計と実装について説明します。
mutexを使ったスレッド安全なキュー
thread_safe_queue.hとして保存する、close可能なキューです。close後は追加を拒否し、既存の要素を取り出し終えたpopがfalseを返します。空かどうかの確認と取り出しを別操作にせず、一つのロック内で判断します。
#ifndef THREAD_SAFE_QUEUE_H
#define THREAD_SAFE_QUEUE_H
#include <condition_variable>
#include <mutex>
#include <queue>
#include <utility>
template<class T>
class ThreadSafeQueue {
public:
bool push(T value) {
{
std::lock_guard<std::mutex> lock(mutex_);
if (closed_) return false;
queue_.push(std::move(value));
}
condition_.notify_one();
return true;
}
bool pop(T& value) {
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] { return closed_ || !queue_.empty(); });
if (queue_.empty()) return false; // close済みで排出完了
value = std::move(queue_.front());
queue_.pop();
return true;
}
void close() {
{
std::lock_guard<std::mutex> lock(mutex_);
closed_ = true;
}
condition_.notify_all();
}
private:
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable condition_;
bool closed_ = false;
};
#endifclose()はすべての待機者を起こし、通知を受けたpopは同じmutexの下で状態を再確認します。キューを破棄する前に利用者をすべて終了させてください。ここではintを使います。一般のTへ広げる場合はムーブ代入の例外保証も検討が必要です。
スレッド安全なキューの利用例
次は3つの生産者と2つの消費者を使います。既出のThreadPoolを別々に用意して、消費者が待機していても生産者が実行できるようにします。
#include "thread_safe_queue.h"
#include "thread_pool.h"
#include <iostream>
#include <stdexcept>
int main() {
ThreadSafeQueue<int> queue;
std::mutex output_mutex;
ThreadPool producers(3);
ThreadPool consumers(2); // 異なるプールで待機による飢餓を避ける
std::vector<std::future<void>> producing, consuming;
try {
for (int id = 0; id < 2; ++id) {
consuming.push_back(consumers.enqueue_task([&] {
int value;
while (queue.pop(value)) {
std::lock_guard<std::mutex> lock(output_mutex);
std::cout << "Consumer popped: " << value << '\n';
}
}));
}
for (int id = 0; id < 3; ++id) {
producing.push_back(producers.enqueue_task([&, id] {
for (int i = 0; i < 10; ++i) {
if (!queue.push(id * 100 + i))
throw std::runtime_error("queue is closed");
}
}));
}
for (auto& result : producing) result.get();
queue.close(); // 最後の生産者が終了してから閉じる
for (auto& result : consuming) result.get();
} catch (...) {
queue.close(); // 例外時にも待機者を起こし、poolのjoinを可能にする
throw;
}
}3つの生産者が計30個を投入し、2つの消費者が重複なく取り出します。生産者の完了後にcloseすると、空のキューで待っている消費者も終了できます。empty()を見てからpopする方式は複数消費者では競合するため採用しません。busy wait用のatomicなdoneフラグも不要です。
スレッド安全なデータ構造の設計原則
スレッド安全なデータ構造を設計する際の基本原則は以下の通りです。
- 排他制御: std::mutexやstd::lock_guardを使用して、クリティカルセクションを保護します。
- 条件変数: std::condition_variableを使って、スレッド間の待機と通知を行います。
- 最小限のロック: パフォーマンスを向上させるために、ロックの粒度を最小限に抑えます。
- コンテナのラップ: 標準ライブラリのコンテナをラップし、スレッド安全なインターフェースを提供します。
これらの原則を守ることで、効率的で安全なスレッド間通信が可能になります。
次のセクションでは、スレッドプログラムのデバッグとテストの方法について解説します。スレッド特有の問題を検出し、修正するためのテクニックを学びましょう。
デバッグとテスト
スレッドプログラミングは、複雑な並行実行のためにデバッグやテストが難しくなることがあります。ここでは、スレッドプログラムのデバッグとテストのための方法とツールについて解説します。
デバッグの基本戦略
スレッドプログラムのデバッグを効果的に行うための基本戦略を紹介します。
ログの活用
ログ出力を活用して、スレッドの実行状態やデータの流れを記録します。ログには、スレッドIDやタイムスタンプを含めると、問題の特定が容易になります。
#include <chrono>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
void log_message(const std::string& message) {
static std::mutex output_mutex;
const auto now = std::chrono::system_clock::now();
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
std::lock_guard<std::mutex> lock(output_mutex);
std::cout << std::this_thread::get_id() << " [epoch_ms="
<< millis << "] " << message << '\n';
}スレッドの可視化
Visual Studioの並列スタックなどで、各スレッドの呼び出し履歴や待機箇所を確認できます。ただし一時点の表示だけで競合がないとは判断できません。デッドロックでは全スレッドが保持・待機しているロックと、条件変数の終了条件を併せて追います。
デッドロックの検出
デッドロックは、複数のスレッドが互いにロックを待ち続ける状態です。デッドロックを避けるために、ロックの順序を統一する、std::lockを使用するなどの対策を講じます。
#include <mutex>
std::mutex mtx1, mtx2;
void thread1() {
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// クリティカルセクション
}
void thread2() {
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// クリティカルセクション
}テストの基本戦略
スレッドプログラムのテストを効果的に行うための基本戦略を紹介します。
ユニットテスト
正常時だけでなく、close後の排出、空のclose、追加拒否もテストします。以下は外部テストライブラリなしで動くassertの例です。NDEBUGを定義せずに実行してください。Google TestやCatch2に同じ検証条件を移しても構いません。
#include "thread_safe_queue.h"
#include <cassert>
int main() {
ThreadSafeQueue<int> queue;
assert(queue.push(1));
queue.close();
assert(!queue.push(2));
int value = 0;
assert(queue.pop(value) && value == 1); // close後も残りは取り出せる
assert(!queue.pop(value)); // 排出後は待機せず終了
queue.close(); // 二度閉じてもよい
}ストレステスト
一定数を投入し、すべて取り出して終了できるかを確認します。回数を増やすだけでなく、複数生産者・複数消費者、空の状態でのclose、例外発生を組み合わせて実施してください。以下は10万件の最小例です。
#include "thread_safe_queue.h"
#include "thread_joiner.h"
#include <cassert>
#include <thread>
int main() {
ThreadSafeQueue<int> queue;
const int operations = 100000;
int received = 0;
std::thread consumer([&] {
int value;
while (queue.pop(value)) ++received;
});
ThreadJoiner guard(consumer);
try {
for (int i = 0; i < operations; ++i) queue.push(i);
queue.close();
} catch (...) {
queue.close();
throw;
}
consumer.join();
assert(received == operations);
}競合状態のテスト
同時更新の結果が期待値になることをassertで確認します。以下はロックで保護した増加処理の回帰テストであり、あらゆる競合を検出するテストではありません。テストが何回通っても、データ競合がないことの証明にはなりません。
#include <cassert>
#include <iostream>
#include <mutex>
#include <thread>
#include "thread_joiner.h"
void race_condition_test() {
int shared_data = 0;
std::mutex mtx;
auto increment = [&shared_data, &mtx]() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++shared_data;
}
};
std::thread t1(increment);
ThreadJoiner guard1(t1);
std::thread t2(increment);
ThreadJoiner guard2(t2);
t1.join();
t2.join();
assert(shared_data == 200000);
std::cout << "Final value: " << shared_data << std::endl;
}
int main() {
race_condition_test();
return 0;
}この例では、shared_dataが正しくインクリメントされることを確認します。
次のセクションでは、スレッドを使ったGUIアプリケーションの実装例を解説します。スレッドを使用して、ユーザーインターフェースの応答性を向上させる方法を学びましょう。
ThreadSanitizerと停止条件のテスト
対応環境ではThreadSanitizerで実行時のデータ競合を調べられます。例えばClangでは次のように計測用バイナリを作ります。検証環境で実行し、通常の性能測定とは分けてください。検出がゼロでも未実行の経路の安全性は保証されず、デッドロックや寿命の設計確認も別途必要です。
clang++ -std=c++11 -g -O1 -fsanitize=thread -pthread main.cpp -o thread_test
./thread_testClangの公式資料で対応OS・制約を確認してください。ThreadSanitizerはこの編集時の環境では実行検証していません。
応用: GUIアプリケーション
スレッドを使用することで、GUIアプリケーションの応答性を向上させることができます。これにより、長時間かかる処理をバックグラウンドで実行しつつ、ユーザーインターフェースをスムーズに保つことができます。ここでは、C++でのスレッドを使ったGUIアプリケーションの実装例を紹介します。
Qtを使ったスレッド処理の例
Qtは、C++でのクロスプラットフォームのGUIアプリケーション開発をサポートする強力なフレームワークです。QtのQThreadクラスを使用して、スレッドを簡単に管理できます。以下に、Qtを使ったスレッド処理の例を示します。
基本的な構造
以下はQtのworker-object方式で、時間のかかる処理を模擬する例です。実際のファイル読込は実装していません。QThreadのオブジェクト自体は作成元のスレッドに属するため、WorkerをmoveToThreadし、queued connectionで依頼します。Qtの例は標準ライブラリだけではビルドできず、Qtの開発環境、moc・uic・qmakeが必要です。
- プロジェクトの設定:
プロジェクトファイル (.pro) に必要なモジュールを追加します。
QT += core gui widgets
CONFIG += c++17
SOURCES += main.cpp mainwindow.cpp worker.cpp
HEADERS += mainwindow.h worker.h
FORMS += mainwindow.ui- Workerクラスの実装:
長時間かかる処理を実行するクラスを作成します。
// worker.h
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork();
signals:
void workFinished();
};
#endif
// worker.cpp
#include "worker.h"
#include <QThread>
void Worker::doWork() {
// ファイル読込の代わりの模擬処理。実処理も適切な区切りで中断確認する。
for (int i = 0; i < 50; ++i) {
if (QThread::currentThread()->isInterruptionRequested()) break;
QThread::msleep(100);
}
emit workFinished();
}- MainWindowクラスの実装:
メインウィンドウクラスで、ワーカースレッドを管理します。
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QThread>
#include "worker.h"
namespace Ui { class MainWindow; }
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
~MainWindow();
signals:
void startWork();
private slots:
void on_startButton_clicked();
void onWorkFinished();
private:
Ui::MainWindow* ui;
QThread* workerThread;
Worker* worker;
};
#endif
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent), ui(new Ui::MainWindow),
workerThread(new QThread(this)), worker(new Worker) {
ui->setupUi(this);
worker->moveToThread(workerThread);
connect(workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &MainWindow::startWork, worker, &Worker::doWork,
Qt::QueuedConnection);
connect(worker, &Worker::workFinished, this, &MainWindow::onWorkFinished,
Qt::QueuedConnection);
workerThread->start(); // ウィンドウの生存中はイベントループを維持
}
MainWindow::~MainWindow() {
workerThread->requestInterruption();
workerThread->quit();
workerThread->wait(); // Workerが終了する前にUIやQThreadを破棄しない
delete ui;
}
void MainWindow::on_startButton_clicked() {
ui->startButton->setEnabled(false);
ui->statusLabel->setText("Working...");
emit startWork();
}
void MainWindow::onWorkFinished() {
ui->statusLabel->setText("Work finished!");
ui->startButton->setEnabled(true); // 同じWorkerに再度依頼できる
}- UIの設定:
Qt Designerを使用して、UIにボタンとラベルを配置します。
<!-- mainwindow.uiの例 -->
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Thread Example</string>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QPushButton" name="startButton">
<property name="geometry">
<rect>
<x>150</x>
<y>110</y>
<width>100</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Start Work</string>
</property>
</widget>
<widget class="QLabel" name="statusLabel">
<property name="geometry">
<rect>
<x>150</x>
<y>160</y>
<width>100</width>
<height>30</height>
</rect>
</property>
<property name="text">
<string>Status: Idle</string>
</property>
</widget>
</widget>
</widget>
<resources/>
<connections/>
</ui>- main.cppの実装:
アプリケーションのエントリポイントを実装します。
// main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}この例ではWorkerを毎回破棄せず、ボタンの連打を抑えて再利用します。終了時に中断要求を出し、Workerがそれを確認した後でwaitします。quit()やrequestInterruption()だけでは実行中の関数を強制停止しません。実際のI/Oには期限やキャンセル対応が必要です。UI更新はGUIスレッドのスロットだけで行います。Qt QThreadとThreads and QObjectsを参照してください。Qt例はソースレビューのみで、実環境でのビルド・画面操作は未検証です。
実装のポイント
- QThreadの使用: QtのQThreadクラスを使うことで、簡単にスレッドを作成し、スレッド内での処理を管理できます。
- シグナルとスロット: Qtのシグナルとスロット機構を使って、スレッド間の通信を簡単に実装できます。
- UIの応答性: バックグラウンドで長時間かかる処理を実行しつつ、UIスレッドはユーザーの操作に対して応答し続けるようにします。
次のセクションでは、本記事のまとめを行います。C++でのstd::threadを使ったスレッドの作成と管理方法の総括を行いましょう。
C++20以降ならstd::jthreadも検討する
std::jthreadは破棄時に、joinableなら停止要求を出してjoinします。ただし強制停止ではなく、処理側がstop_tokenを確認する協調停止です。停止を無視する処理や無期限I/Oではデストラクタも待ち続けます。例外処理と共有データの同期は別途必要です。jthreadの仕様を参照してください。
#include <chrono>
#include <thread>
int main() {
std::jthread worker([](std::stop_token stop) {
while (!stop.stop_requested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
});
worker.request_stop();
} // joinableならデストラクタが停止要求を出してjoinまとめ
本記事では、C++でのstd::threadを使ったスレッドの作成と管理方法について、基本から応用までを体系的に解説しました。スレッドを使うことで、プログラムの並行処理能力を向上させ、効率的にリソースを活用することが可能です。以下に、各セクションのポイントをまとめます。
- std::threadの基本: スレッドの作成と基本的な使い方について学びました。スレッドの生成、ラムダ式を使ったスレッドの作成、引数付きスレッドの作成方法を紹介しました。
- スレッドの結合と分離: スレッドの結合(join)と分離(detach)の違いと、それぞれの使用方法について説明しました。
- スレッドの同期: std::mutexやstd::lock_guardを使ったスレッドの同期方法を解説しました。また、unique_lockを使った柔軟な同期方法も紹介しました。
- 条件変数の利用: std::condition_variableを使ったスレッド間の待機と通知の方法について学びました。条件変数を使って効率的にスレッドを同期する方法を紹介しました。
- スレッドプールの実装: スレッドプールを実装して、複数のタスクを効率的に管理する方法を学びました。スレッドプールの基本構造と利用方法について説明しました。
- 実践例: 並列計算: 配列の要素の合計を並列に計算する例を通して、実際の並列計算の方法を学びました。スレッド数の動的調整も紹介しました。
- 実践例: Webサーバー: スレッドを使った簡易Webサーバーの実装方法を紹介しました。Boost.Asioを使ってネットワークプログラミングを行い、スレッドプールを活用する方法も説明しました。
- スレッド安全なデータ構造: スレッド安全なキューの実装方法を学びました。mutexとcondition_variableを使ってスレッド間のデータ競合を防ぐ方法を紹介しました。
- デバッグとテスト: スレッドプログラムのデバッグとテストの基本戦略について説明しました。ログの活用、デッドロックの検出、ユニットテスト、ストレステストの方法を紹介しました。
- 応用: GUIアプリケーション: Qtを使ったスレッド処理の例を通して、GUIアプリケーションにおけるスレッドの利用方法を学びました。QThreadとシグナル・スロット機構を使って、ユーザーインターフェースの応答性を向上させる方法を紹介しました。
これらの知識を活用することで、C++でのスレッドプログラミングの理解を深め、より効率的で信頼性の高いプログラムを作成できるようになるでしょう。スレッドプログラミングは複雑でありながらも強力な技術であり、適切な仕事の分割、寿命管理、終了条件と実測をそろえて使うことが重要です。
参照確認日:2026年10月20日。標準ライブラリ中心の例はGCCでローカル検証し、Boost.AsioとQtの例は公式資料とのソース照合にとどめています。対象サービスでの通信、公開サーバー運用、Qt GUI動作の検証を示すものではありません。



コメント