C++のスレッド処理を7つのサンプルコードで完全ガイド

C++でのスレッド処理の完全ガイドの画像C++
この記事は約18分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

C++のスレッド処理は、プログラミングの多様な側面で活用されます。

この記事では、C++におけるスレッド処理の基本から応用までを深く掘り下げていきます。

初心者でも分かりやすい説明と具体的なサンプルコードを交えながら、スレッド処理の全体像を把握し、実際にプログラミングで活用するスキルを身につけることが目的です。

マルチスレッドプログラミングが初めての方でも、この記事を通じてC++のスレッド処理について理解を深めることができるでしょう。

●C++とスレッド処理の基本

C++でのスレッド処理は、プログラム内で複数の処理を並行して実行するための重要なテクニックです。

特に、複数のタスクを同時に処理することで、アプリケーションのパフォーマンスを向上させることができます。

しかし、スレッド処理を正しく理解し適切に使用しないと、予期しないエラーや問題が発生する可能性があります。

C++では、C++11以降のバージョンで標準ライブラリとしてスレッドがサポートされており、より安全かつ効率的にスレッドを扱うことが可能になりました。

ここでは、C++のスレッド処理における基本的な概念と用途について詳しく解説します。

○スレッド処理とは何か?

スレッド処理とは、プログラム内で複数の処理を同時に実行する手法です。

一般的に、プログラムは単一のスレッドで実行されますが、スレッド処理を用いることで、複数のスレッドを同時に動かすことができます。

これにより、各スレッドが異なるタスクを同時に処理することで、マルチタスク処理が可能になります。

C++においてスレッド処理を利用する主な目的は、計算資源を効率的に利用し、プログラムの実行効率を高めることです。

○C++でスレッドを使うメリット

C++でスレッドを使うことの最大のメリットは、アプリケーションのパフォーマンスを大幅に向上させることができる点です。

特に、CPUの複数のコアを効果的に活用することで、並列処理の効率を高めることが可能です。

また、一部の処理がブロックされている間に他のスレッドが別の作業を進めることで、全体の応答時間の短縮にも寄与します。これにより、ユーザーエクスペリエンスの向上にもつながります。

しかし、スレッド間のデータ共有や同期の問題、競合状態などの複雑な問題も伴いますので、スレッドを適切に管理する知識が必要です。

●基本的なスレッドの使い方

C++でのスレッド処理は、プログラムの並行処理を可能にし、アプリケーションのパフォーマンスを高める重要な技術です。

ここでは、C++11から導入されたスレッドライブラリを使った基本的なスレッドの作成方法と、スレッド間でデータを安全にやり取りする方法を、具体的なサンプルコードを交えて説明します。

○サンプルコード1:C++11でスレッドを作成する基本

C++11では、ライブラリを使って簡単にスレッドを作成できます。

基本的なスレッドの作成は、ヘッダをインクルードした後、std::threadオブジェクトを生成し、実行したい関数を引数に渡すだけです。

下記のサンプルコードは、単純なスレッドを作成し、実行する一連の流れを表しています。

#include <iostream>
#include <thread>

// スレッドで実行する関数
void threadFunction() {
    std::cout << "スレッド開始\n";
    // スレッドの作業をここに記述
    std::cout << "スレッド終了\n";
}

int main() {
    std::thread t(threadFunction); // スレッドの作成
    t.join(); // スレッドの終了を待機
    return 0;
}

このコードでは、threadFunctionという関数を新しいスレッドで実行しています。

std::threadオブジェクトのtが作成された時点で、スレッドが開始されます。

t.join()は、メインスレッドが新しく作成されたスレッドの処理が終わるまで待機するために必要です。

これにより、メインスレッドと新しいスレッドの並行実行が可能になります。

○サンプルコード2:スレッドにデータを渡す方法

スレッドにデータを渡す際は、関数の引数を介してデータをスレッド関数に渡します。

下記のサンプルコードでは、スレッドに整数値を渡し、その値を使って処理を行う方法を表しています。

#include <iostream>
#include <thread>

// スレッドで実行する関数。引数として整数値を受け取る
void threadFunction(int value) {
    std::cout << "スレッドが受け取った値: " << value << "\n";
    // ここで引数を使用した処理を行う
}

int main() {
    int value = 10;
    std::thread t(threadFunction, value); // 引数を持つスレッドの作成
    t.join(); // スレッドの終了を待機
    return 0;
}

この例では、main関数内で定義されたvalue変数の値を、スレッド関数threadFunctionに渡しています。

この方法を利用すれば、外部からスレッドに任意のデータを渡すことができ、より柔軟なスレッドプログラミングが可能になります。

重要なのは、スレッド間でのデータの共有と同期を適切に行うことです。

データの不整合や競合を避けるために、mutexやロック、条件変数などの同期メカニズムの使用が推奨されます。

●スレッド処理における同期とは

スレッド処理における同期とは、複数のスレッドが安全に共有リソースにアクセスするためのメカニズムです。

スレッド間でデータを共有する際に、データの整合性を保ちつつアクセスの衝突を避けるために重要です。

C++では、mutex(相互排他)や条件変数といった同期プリミティブを提供しており、これらを使ってスレッド間の安全なデータの共有と通信を行うことができます。

○サンプルコード3:mutexを使ったスレッド間の同期

mutexを使用すると、複数のスレッドが同時に特定のコードブロックにアクセスすることを防ぐことができます。

これにより、共有されたリソースへの安全なアクセスが可能になります。

下記のサンプルコードは、mutexを使って共有データに対するスレッド間のアクセスを同期する方法を表しています。

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

int sharedData = 0;
std::mutex mtx;

void incrementSharedData() {
    mtx.lock();  // ミューテックスのロック
    ++sharedData;  // 共有データのインクリメント
    std::cout << "データが更新されました: " << sharedData << std::endl;
    mtx.unlock();  // ミューテックスのアンロック
}

int main() {
    std::thread t1(incrementSharedData);
    std::thread t2(incrementSharedData);
    t1.join();
    t2.join();
    return 0;
}

この例では、共有されたデータsharedDataへのアクセスを、mutexオブジェクトmtxを使用して制御しています。

この方法により、一度に一つのスレッドのみがsharedDataを変更することが保証され、データの整合性が維持されます。

○サンプルコード4:条件変数を活用したスレッド通信

条件変数は、ある条件が満たされるまでスレッドを待機させるために使用されます。

これにより、スレッド間での協調動作が可能になります。

下記のサンプルコードでは、条件変数を使用して、あるスレッドが処理を完了するまで別のスレッドを待機させる方法を表しています。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void workerThread() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; });  // 条件が満たされるまで待機
    std::cout << "作業を開始\n";
    // ここに作業内容を記述
}

int main() {
    std::thread worker(workerThread);
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
        cv.notify_one();  // 条件変数を通じてworkerスレッドに通知
    }
    worker.join();
    return 0;
}

このコードでは、workerThread関数が条件変数cvを用いて待機し、main関数からの通知を受け取るまで処理を開始しません。

これにより、必要な準備が整うまでの間、workerThreadが無駄な処理を行うことなく、効率的に作業を開始することができます。

条件変数は、スレッド間での協調を効果的に行うための強力なツールです。

●高度なスレッド処理のテクニック

C++の高度なスレッド処理においては、futureとpromiseを活用することで、スレッド間のデータの受け渡しや、非同期処理の結果を扱うことが可能です。

これらの機能を利用することで、スレッド間の通信やデータ共有をより柔軟に、効率的に行うことができます。

○サンプルコード5:futureとpromiseを使ったスレッド間のデータ受け渡し

futureとpromiseを用いたスレッド間のデータ受け渡しの例を紹介します。

promiseはあるスレッドで値を設定し、その値をfutureを通じて別のスレッドで取得するために使用されます。

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

void compute(std::promise<int> &promiseObj) {
    int result = 2 * 2; // 何かの計算を行う
    promiseObj.set_value(result); // 計算結果をpromiseにセット
}

int main() {
    std::promise<int> promiseObj;
    std::future<int> futureObj = promiseObj.get_future();
    std::thread th(compute, std::ref(promiseObj));

    int result = futureObj.get(); // futureから計算結果を取得
    std::cout << "計算結果: " << result << std::endl;

    th.join();
    return 0;
}

このサンプルコードでは、compute関数内で行われた計算結果をpromiseObjを通じてセットし、メインスレッド内でその値をfutureObjを介して取得しています。

future.get()メソッドは、対応するpromiseの値がセットされるまで待機し、値が利用可能になったらそれを返します。

これにより、非同期処理の結果を効率的にスレッド間でやり取りすることが可能になります。

●よくあるエラーとその対処法

マルチスレッドプログラミングにおいては、特有のエラーや問題が発生する可能性があります。

特にレースコンディションやデッドロックは、マルチスレッド環境において最も一般的な問題の一つです。

これらの問題を理解し、適切に対処することが重要です。

○レースコンディションとは

レースコンディションは、複数のスレッドが同時に共有データにアクセスし、その結果が実行のタイミングに依存してしまう状況を指します。

これにより、プログラムの動作が予測不可能になったり、データが破壊される可能性があります。

レースコンディションを避けるためには、共有データへのアクセスを適切に制御する必要があります。

例えば、mutexやセマフォを使用して、一度に一つのスレッドのみが共有データにアクセスできるようにすることが挙げられます。

○デッドロックの避け方

デッドロックは、複数のスレッドがお互いに相手がリソースを解放するのを無限に待ってしまう状態を指します。

これにより、プログラム全体が停止することになります。

デッドロックを避けるためには、リソースの取得順序を一貫させる、ロックを取得する際にタイムアウトを設定する、必要最小限のロックを使用するといった方法が有効です。

また、ロックの取得順序を明確にすることで、デッドロックの発生を防ぐことができます。

●C++スレッド処理の応用例

C++におけるスレッド処理は、その性質上、さまざまな応用例が存在します。

特にマルチスレッドを利用したデータ処理や、効率的なリソース管理のためのスレッドプールの実装は、多くの現代的なアプリケーションにおいて重要な役割を果たしています。

○サンプルコード6:マルチスレッドを使ったデータ処理

マルチスレッドを利用することで、データ処理を複数のスレッドに分散し、処理の高速化を図ることができます。

下記のサンプルコードでは、複数のスレッドを生成して、それぞれがデータの一部を処理するシンプルな例を表しています。

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

void processData(const std::vector<int>& data, int start, int end) {
    for (int i = start; i < end; ++i) {
        // データの処理をここで実行
        std::cout << "データ処理: " << data[i] << std::endl;
    }
}

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::thread t1(processData, std::ref(data), 0, 5);
    std::thread t2(processData, std::ref(data), 5, 10);

    t1.join();
    t2.join();
    return 0;
}

このコードでは、データセットを半分に分割し、2つのスレッドでそれぞれ処理を行っています。

このようにデータを分割することで、全体の処理時間を短縮することが可能です。

○サンプルコード7:スレッドプールの実装例

スレッドプールは、事前に決められた数のスレッドをプールし、複数のタスクを効率良く処理するために用いられます。

下記のサンプルコードは、基本的なスレッドプールの実装を表しています。

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

class ThreadPool {
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;

    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;

public:
    ThreadPool(size_t threads) : stop(false) {
        for (size_t i = 0; i < threads; ++i)
            workers.emplace_back(
                [this] {
                    while (true) {
                        std::function<void()> task;
                        {
                            std::unique_lock<std::mutex> lock(this->queue_mutex);
                            this->condition.wait(lock,
                                [this] { return this->stop || !this->tasks.empty(); });
                            if (this->stop && this->tasks.empty())
                                return;
                            task = std::move(this->tasks.front());
                            this->tasks.pop();
                        }
                        task();
                    }
                }
            );
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread &worker : workers)
            worker.join();
    }

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) 
        -> std::future<typename std::result_of<F(Args...)>::type> {
        using return_type = typename std::result_of<F(Args...)>::type;

        auto task = std::make_shared< std::packaged_task<return_type()> >(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...)
        );

        std::future<return_type> res = task->get_future();
        {
            std::unique_lock<std::mutex> lock(queue_mutex);

            // ストップされた場合はタスクを受け付けない
            if (stop)
                throw std::runtime_error("enqueue on stopped ThreadPool");

            tasks.emplace([task](){ (*task)(); });
        }
        condition.notify_one();
        return res;
    }
};

このスレッドプールでは、複数のワーカースレッドがタスクキューからタスクを取得し、実行します。

タスクは動的に追加され、スレッドプールの管理下で効率的に処理されます。

これにより、リソースの有効活用と性能の最適化を図ることができます。

●エンジニアが知っておくべきC++スレッド処理の豆知識

C++のスレッド処理においてエンジニアが知っておくべき重要なポイントは、プログラムの性能と効率性を高めるための方法論に大きく関わります。

ここでは、特にスレッドの効率的な使い方とC++20で追加された新しいスレッド機能に焦点を当てます。

○豆知識1:スレッドの効率的な使い方

効率的なスレッドの使用は、プログラムの性能を最適化する上で不可欠です。

重要なのは、スレッド数を適切に管理し、ハードウェアリソースを最大限に活用することです。

過剰なスレッドの作成は避け、データの共有とスレッド間の同期を慎重に行う必要があります。

また、スレッドのライフサイクル管理を適切に行い、スレッドプールを利用することで効率的なリソース管理が可能になります。

○豆知識2:C++20の新しいスレッド機能

C++20はスレッド処理の領域において、いくつかの新しい機能を導入しました。

「std::jthread」は自動的にスレッドを終了させる新しいクラスであり、スレッドの終了処理を容易にします。

セマフォは特定数の同時アクセス制御に役立ち、より細かなリソース管理を可能にします。

ラッチとバリアは、複数スレッド間の同期点での待機と同時再開を管理する新しい同期メカニズムです。

これらの新機能により、C++におけるマルチスレッドプログラミングはより柔軟かつ強力なものとなります。

まとめ

このC++スレッド処理のガイドでは、スレッド処理の基礎から高度なテクニックまでを幅広くカバーしました。

初心者から中級者までが、スレッドの作成、データの受け渡し、同期処理の方法、さらにはC++20における最新の機能に至るまでを理解できるように構成されています。

エラーの対処法や、効率的なスレッド使用のコツも含め、C++におけるマルチスレッドプログラミングの理解を深めるための貴重な資料となることでしょう。

この情報が、あなたのプロジェクトや学習に役立つことを願っています。