記事内にはプロモーションが含まれています

【C++】並列処理で高速化!std::threadの使い方からOpenMP、GPU活用まで

【C++】並列処理で高速化!std::threadの使い方からOpenMP、GPU活用まで C++

CPUのマルチコア化が進んだ現代において、プログラムの処理速度を最大限に引き出すためには「並列処理(マルチスレッド)」の活用が欠かせません。

C++はハードウェアの性能を限界まで引き出せる言語ですが、並列処理を正しく実装するには、スレッドの管理や排他制御といった専門的な知識が必要です。

「重たい計算処理を高速化したい」
std::thread の使い方がよく分からない」
「OpenMPやGPUを使った並列化にも興味がある」

このように考えている人も多いでしょう。

この記事では、C++における並列処理の基本的な実装方法から、データ競合を防ぐための排他制御、そして既存のコードを簡単に高速化するOpenMPや並列アルゴリズム(C++17以降)まで、現在の実務で役立つテクニックをサンプルコード付きで徹底解説します。

【本記事の信頼性】
プロフィール
執筆者:マヒロ
  • 執筆者は元エンジニア
  • SES⇒大手の社内SE⇒独立
  • 現在はこじんまりとしたプログラミングスクールを運営
  • モットーは「利他の精神」

C++標準ライブラリ(std::thread)を使った基本の並列処理

C++11以降、標準ライブラリに <thread> が導入され、OSに依存しないポータブルなコードでマルチスレッドプログラミングが可能になりました。

まずは、スレッドを作成して並列に動作させる最も基本的な方法を見ていきましょう。

スレッドの作成と実行(join)

新しいスレッドを作るには、std::thread クラスを使用します。

コンストラクタに関数(または関数オブジェクト、ラムダ式)を渡すだけで、その処理が別スレッドで走り出します。

#include <iostream>
#include <thread>
#include <chrono>

// 別スレッドで実行させたい関数
void worker_task(int id) {
    for (int i = 0; i < 3; ++i) {
        std::cout << "Thread " << id << ": 処理中... (" << i + 1 << "/3)" << std::endl;
        // 1秒待機(重い処理のシミュレーション)
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int main() {
    std::cout << "メインスレッド開始" << std::endl;

    // スレッドt1, t2を作成して開始
    std::thread t1(worker_task, 1);
    std::thread t2(worker_task, 2);

    std::cout << "メインスレッドは待機します" << std::endl;

    // join()を呼んでスレッドの終了を待つ(必須)
    t1.join();
    t2.join();

    std::cout << "全スレッド終了" << std::endl;
    return 0;
}

実行結果(例)
※出力の順序はタイミングによって変わります。

メインスレッド開始
メインスレッドは待機します
Thread 1: 処理中... (1/3)
Thread 2: 処理中... (1/3)
Thread 1: 処理中... (2/3)
Thread 2: 処理中... (2/3)
Thread 1: 処理中... (3/3)
Thread 2: 処理中... (3/3)
全スレッド終了

std::thread t1(worker_task, 1); と記述した瞬間に新しいスレッドが生成され、worker_task(1) が並列に実行されます。

重要なのは t1.join(); です。

これは「スレッド t1 の処理が終わるまで、メインスレッド(呼び出し元)を待機させる」という命令です。

もし join() を忘れると、メイン関数が先に終了してしまい、動作中のスレッドが強制終了させられてエラー(std::terminate)が発生します。
必ず終了待ちを行うようにしましょう。

データ競合を防ぐ「排他制御」(Mutex)

並列処理で最も注意すべきなのが 「データ競合(Data Race)」 です。

複数のスレッドが「同じ変数」に対して同時に書き込みを行うと、データの不整合や予期せぬ動作を引き起こします。

これを防ぐために、std::mutex を使って「排他制御」を行います。

mutexとlock_guardを使った安全な書き込み

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

int counter = 0;       // 共有リソース
std::mutex mtx;        // 排他制御用のミューテックス

void count_up() {
    for (int i = 0; i < 10000; ++i) {
        // ロックを取得(スコープを抜けると自動解放)
        std::lock_guard<std::mutex> lock(mtx);
        
        // クリティカルセクション(一度に1つのスレッドしか実行できない領域)
        counter++;
    }
}

int main() {
    std::vector<std::thread> threads;

    // 10個のスレッドで同時にカウントアップ
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(count_up);
    }

    // 全スレッドの終了を待機
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "最終的なカウント: " << counter << std::endl;
    return 0;
}

実行結果

最終的なカウント: 100000

もし std::lock_guard がなかった場合、複数のスレッドが同時に counter++ を行い、上書きし合ってしまい、結果が 100000 にならないことが多々あります。

std::lock_guard<std::mutex> lock(mtx); を記述することで、そのブロック内(クリティカルセクション)は一度に一つのスレッドしか入れなくなります。

これによりデータの整合性が保たれます。

lock_guard はデストラクタで自動的にロックを解除してくれるため、解除忘れ(デッドロック)を防ぐのに便利です。

戻り値を取得するなら std::async と std::future

std::thread は強力ですが、スレッドからの戻り値を受け取るのが少し面倒です。

計算結果を受け取りたい場合は、std::async を使うとより簡単に非同期処理(タスク並列)を実装できます。

非同期タスクの実行と結果の取得

#include <iostream>
#include <future>
#include <thread>

// 重い計算を行う関数
int heavy_calculation(int x, int y) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return x + y;
}

int main() {
    std::cout << "計算開始..." << std::endl;

    // std::asyncで非同期実行を開始
    // std::launch::async は「必ず別スレッドで実行」を指定
    std::future<int> result_future = std::async(std::launch::async, heavy_calculation, 10, 20);

    std::cout << "メインスレッドで他の処理を実行中..." << std::endl;

    // get()を呼ぶと、計算が終わるまで待機し、結果を取得する
    int result = result_future.get();

    std::cout << "計算結果: " << result << std::endl;
    return 0;
}

実行結果

計算開始...
メインスレッドで他の処理を実行中...
計算結果: 30

std::async は、処理の「未来の結果」を表す std::future オブジェクトを返します。

result_future.get() を呼び出した時点で、もし計算が終わっていなければ完了するまで待機し、終わっていれば即座に値を取り出します。

スレッドの管理をライブラリ側に任せられるため、単純な並列計算であれば std::thread よりも推奨されます。

お手軽に高速化!OpenMPによる並列化

科学技術計算や画像処理の分野で古くから使われているのが OpenMP です。

コンパイラへの指示(プラグマ)を1行書くだけで、既存の for ループなどを簡単に並列化できるのが最大の特徴です。

forループを並列化する

使用する際は、コンパイルオプション(GCCなら -fopenmp、Visual Studioならプロパティで有効化)が必要です。

#include <iostream>
#include <vector>
#include <omp.h> // OpenMPヘッダ

int main() {
    const int N = 100000;
    std::vector<int> data(N);

    // #pragma omp parallel for を書くだけで並列化される
    #pragma omp parallel for
    for (int i = 0; i < N; ++i) {
        data[i] = i * i;
    }

    std::cout << "並列処理完了" << std::endl;
    return 0;
}

#pragma omp parallel for というディレクティブ(指示行)を書くと、その直後の for ループが自動的に複数のスレッドに分割されて実行されます。

スレッドの生成や管理をコードで書く必要が一切ないため、既存コードの高速化においては非常に強力な選択肢となります。

C++17以降の並列アルゴリズムとGPU活用

2026年の現在、C++標準ライブラリも進化し、アルゴリズム関数(std::sortstd::for_each)に「実行ポリシー」を指定することで並列化できるようになっています。

並列アルゴリズム(Parallel Algorithms)

C++17で導入された std::execution を使います。

#include <iostream>
#include <vector>
#include <algorithm>
#include <execution> // 実行ポリシー用

int main() {
    std::vector<int> vec(1000000);
    // データ埋め
    for(int i=0; i<1000000; ++i) vec[i] = 1000000 - i;

    // 並列ソートを実行 (std::execution::par)
    std::sort(std::execution::par, vec.begin(), vec.end());

    std::cout << "ソート完了: " << vec[0] << ", " << vec[1] << "..." << std::endl;
    return 0;
}

std::execution::par を指定するだけで、処理系が自動的に並列処理(マルチスレッドやベクトル化)を行ってくれます。

さらに par_unseq を指定すれば、CPUのSIMD命令を活用したベクトル並列化が行われます(※特定のコンパイラを使用すればGPUへのオフロードも可能です)。

GPUによる並列処理について

さらに大規模な並列処理(ディープラーニングや物理シミュレーション)を行う場合は、CPUではなく GPU を活用します。

C++でGPU並列処理を行うには、以下のようなフレームワークを使用するのが一般的です。

  • CUDA (NVIDIA): NVIDIA製GPU専用。C++を拡張した言語で記述。
  • SYCL / oneAPI: クロスプラットフォームな異種構造計算フレームワーク。標準C++に近い記述が可能。
  • C++ AMP: Microsoftが提唱したGPU汎用計算ライブラリ(現在は枯れ気味)。

これらは導入のハードルが高いですが、単純なマルチスレッドとは桁違いの並列数(数千〜数万スレッド)を扱えるため、処理内容によっては劇的な高速化が見込めます。

C++のスキルを活かして年収を上げる方法

以上、C++で並列処理を使って高速化する方法について解説しました。

なお、C++のスキルがある場合には、「転職して年収をアップさせる」「副業で稼ぐ」といった方法を検討するのがおすすめです。

C++を扱えるエンジニアは希少価値が高いため、転職によって数十万円の年収アップはザラで、100万円以上年収が上がることも珍しくありません。

なお、転職によって年収を上げたい場合は、エンジニア専門の転職エージェントサービスを利用するのが最適です。

今すぐ転職する気がなくとも、とりあえず転職エージェントに無料登録しておくだけで、スカウトが届いたり、思わぬ好待遇の求人情報が送られてきたりするというメリットがあります。

併せて、副業案件を獲得できるエージェントにも登録しておくと、空いている時間を活かして稼げるようなC++の案件を探しやすくなります。

転職エージェントも副業エージェントも、登録・利用は完全無料なので、どんな求人や副業案件があるのか気になる方は、気軽に利用してみるとよいでしょう。