導入
マルチコアプロセッサの普及に伴い、マルチスレッドプログラミングは特定分野の専門技術ではなくなり、現代のソフトウェア開発においてハードウェアの性能を最大限に活用し、アプリケーションの応答性を向上させるための重要な手段となっています。C++11標準以降、C++言語は標準ライブラリレベルでマルチスレッドの完全なサポートを提供しており、開発者は移植可能で標準化された方法で並行プログラムを記述できるようになりました。本記事では、C++マルチスレッドの核心概念、基本的使用法、重要な同期メカニズムを体系的に整理し、主要機能の詳細な使用法を解説するとともに、スレッドの戻り値や例外処理などの高度なトピックを補足します。
1. マルチスレッドの核心概念
コードに深入りする前に、いくつかの核心概念を明確にする必要があります。
1.1 プロセスとスレッド (Process & Thread)
- プロセス (Process):オペレーティングシステムがリソースの割り当てとスケジューリングを行う基本単位です。プロセスは独立したメモリ空間、データスタック、およびその他のシステムリソースを持ちます。プロセス間の通信(IPC)は比較的複雑でオーバーヘッドが大きいです。
- スレッド (Thread):プロセス内の実行単位であり、CPUスケジューリングの最小単位です。一つのプロセスは複数のスレッドを含むことができ、これらはプロセスのメモリ空間(コードセグメント、データセグメント)とリソースを共有します。したがって、スレッド間の通信はより効率的ですが、データ同期の課題も導入されます。
1.2 同時実行と並列実行 (Concurrency & Parallelism)
- 同時実行 (Concurrency):一つの時間枠内で、複数のタスクが進行している状態を指します。単一コアプロセッサでは、これは時間スライスのローテーションなどの方法で実現される宏观的な並列処理です。タスク間は交互に実行されます。
- 並列実行 (Parallelism):同一時刻に複数のタスクが同時に実行される状態を指します。これはマルチコアプロセッサの物理的なサポートを必要とし、真の意味での同時実行です。
C++マルチスレッドプログラミングの目標は、ハードウェアを利用して並列処理を実現し、または論理的に同時実行タスクを管理することです。
1.3 競合状態とクリティカルセクション (Race Condition & Critical Section)
- 競合状態 (Race Condition):複数のスレッドが共有データにアクセスし、少なくとも一つのスレッドがデータを変更する場合、最終結果がスレッドの実行時の特定の順序に依存する場合、競合状態が発生したと言います。これは通常、プログラムの動作が予測不能になる原因となり、並行プログラミングで最も一般的なエラーの源です。
- クリティカルセクション (Critical Section):共有リソース(共有データなど)にアクセスするコードのセグメントを指します。競合状態を避けるため、任意の時点で一つのスレッドしかクリティカルセクションに入ることができないようにする必要があります。
2. <thread> ライブラリの基本使用法
C++マルチスレッド機能は主に <thread> ヘッダーファイルによって提供されます。
2.1 スレッドの作成と起動
新しいスレッドを起動するには、std::thread クラスのインスタンスを作成する必要があります。このインスタンスを作成する際、新しいスレッドがどの関数を実行するべきか、およびその関数に渡すパラメータを指定する必要があります。
使用形式:
#include <iostream>
#include <thread>
#include <stdexcept> // for std::exception
// スレッドエントリ関数
void worker_function(int param) {
std::cout << "ワーカースレッドが実行中、パラメータ: " << param << std::endl;
}
int main() {
try {
// 新しいスレッドを起動し、worker_function(50)を実行
std::thread worker(worker_function, 50);
// ... メインスレッドは引き続き実行 ...
worker.join(); // ワーカースレッドの終了を待機
} catch (const std::system_error& e) {
std::cerr << "システムエラーが発生しました: " << e.what() << '\n';
// 例:システムリソースが不足し、新しいスレッドを作成できない場合
}
return 0;
}
使用法の詳細:
std::threadオブジェクトの作成std::thread オブジェクト名( 実行する関数, 関数の第一引数, 関数の第二引数, ... );
- 第一引数: 呼び出し可能なオブジェクト(Callable Object)、例えば関数名、関数ポインタ、Lambda式、または
()演算子をオーバーロードしたオブジェクト。これが新しいスレッドのエントリポイントを指定します。- 後続の引数: 上記の関数に渡される実引数のリスト。これらの引数は、新しいスレッドの内部ストレージにコピーまたは移動されます。
- 戻り値:
std::threadのコンストラクタには戻り値がありません。- 作成成功の判断: スレッドの作成は失敗する可能性のあるシステムコールです。オペレーティングシステムが新しいスレッドを作成できない場合(例:リソース枯渇)、
std::threadのコンストラクタはstd::system_error例外をスローします。したがって、スレッドの作成をtry-catchブロック内に配置することは堅牢な方法です。- 役割:
threadオブジェクトを作成すると同時に、システムレベルの実行スレッドが起動されます。このスレッドはメインスレッドとは独立し、指定された関数の実行を開始します。
2.2 スレッドのライフサイクル管理
std::thread オブジェクトは、デストラクタ時に実行中のスレッドと関連付けられていない状態でなければなりません。これは、thread オブジェクトが破棄される前に、関連するバックグラウンドスレッドの運命を明確にする必要があることを意味します:終了を待機する(join)か、完全に独立して実行させる(detach)かのいずれかです。
2.2.1 スレッドオブジェクトの結合可能性 (Joinability)
std::thread オブジェクトは、「joinable」である場合、実行中のスレッドを表します。
- デフォルトコンストラクタで作成された
std::threadオブジェクトは non-joinable です。 join()またはdetach()されたstd::threadオブジェクトは non-joinable になります。- 移動(
std::move)されたstd::threadオブジェクトは non-joinable になります。
2.2.2 join() - スレッドの終了を待機
join() はブロッキング操作であり、子スレッドの実行完了を待機し、関連リソースをクリーンアップするために使用されます。
#include
#include
#include
void background_worker() {
std::cout