【C++】スレッド管理の基礎から応用まで!5つのサンプルコードで完全解説 – Japanシーモア

【C++】スレッド管理の基礎から応用まで!5つのサンプルコードで完全解説

C++のスレッド管理を学ぶための詳細なガイドのイメージC++
この記事は約17分で読めます。

 

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

このサービスは複数のSSPによる協力の下、運営されています。

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

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

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

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

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

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

はじめに

この記事では、C++におけるスレッド管理の基礎から応用までを深く掘り下げ、特にthread::joinメソッドに焦点を当てて解説します。

C++はマルチスレッドプログラミングに対応した強力な言語であり、適切なスレッド管理はプログラムの効率と安定性を大きく左右します。

初心者から上級者まで、C++におけるスレッド管理の重要性と効果的な使い方を理解し、実践的な知識を身につけることができます。

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

C++におけるスレッド管理は、プログラムの実行を複数の独立したタスクに分割し、それらを並列に実行することで全体のパフォーマンスを向上させる技術です。

スレッドは、プログラム内で独立した実行の流れを作る最小の単位であり、それぞれが独自の実行コンテキスト(例えば、プログラムカウンタ、レジスタセット、スタック)を持ちます。

○スレッドとは何か?

スレッドは、プロセス内で独立して実行される一連の命令の流れを指します。

一つのプロセスは複数のスレッドを持つことができ、これにより一つのアプリケーション内で複数の操作を同時に行うことが可能になります。

スレッドは、各々がプロセスのリソース(メモリ、オープンファイルなど)を共有しながら動作します。

○C++におけるスレッドの役割

C++では、<thread>ライブラリを使用してスレッドを作成し管理します。

このライブラリはC++11で導入され、それ以降のバージョンで利用可能です。

C++のスレッドは、マルチコアプロセッサの利点を生かし、複数のタスクを同時に処理することでプログラムの応答性と処理速度を高める重要な役割を担います。

例えば、ユーザーインターフェイスの応答性を保ちながら、バックグラウンドでデータ処理を行う場合などに有効です。

●thread::joinの基本

C++のマルチスレッドプログラミングにおいて、thread::joinは非常に重要なメソッドです。

このメソッドは、スレッドが完了するまで待機する機能を提供し、スレッドの終了を確実にするために使用されます。

thread::joinを適切に使用することで、プログラムの不安定性を防ぎ、データの整合性を保つことができます。

スレッドが別の処理を行っている間、メインスレッドはjoinメソッドを呼び出すことによって、そのスレッドの処理が完了するまで待機します。

これにより、複数のスレッドが同時に実行されている場合でも、一つ一つのスレッドが順序良く終了することが保証されます。

○thread::joinとは何か?

thread::joinメソッドは、あるスレッドが終了するまで、呼び出し元のスレッドの実行をブロックします。

このメソッドは、スレッドが実行中のタスクを完了させ、リソースを適切に解放した後にのみ、制御を呼び出し元のスレッドに戻します。

これは、スレッドの終了を同期させる上で非常に重要なプロセスです。

例えば、メインスレッドが複数のワーカースレッドを生成し、それぞれが異なるタスクを実行する場合、メインスレッドは各ワーカースレッドがjoinを通じて終了するまで待機する必要があります。

これにより、全てのワーカースレッドが正常にタスクを完了し、リソースが適切に解放されることを保証することができます。

○基本的な使い方のサンプルコード

ここでは、thread::joinの基本的な使用方法を表すサンプルコードを紹介します。

#include <iostream>
#include <thread>

void workerFunction(int n) {
    // 何らかのタスクを実行する
    std::cout << "スレッドから: " << n << std::endl;
}

int main() {
    std::thread worker(workerFunction, 5); // スレッドを生成し、workerFunctionを実行

    // メインスレッドで他の処理を実行することも可能
    std::cout << "メインスレッドからの出力" << std::endl;

    worker.join(); // workerスレッドが終了するまで待機

    // スレッドが終了した後の処理
    std::cout << "スレッド終了後のメインスレッドからの出力" << std::endl;

    return 0;
}

このコードでは、workerFunctionという関数を新しいスレッドで実行し、main関数(メインスレッド)ではworker.join()を呼び出しています。

これにより、workerFunctionが実行を終了するまで、メインスレッドの実行はブロックされます。

●thread::joinの応用例

C++のthread::joinメソッドは、基本的なスレッド終了の待機機能以上の多くの応用が可能です。

ここでは、その応用例として、マルチスレッド処理の同期、リソース共有の制御、データの整合性の維持などを取り上げます。

○サンプルコード1:マルチスレッド処理の同期

マルチスレッドプログラムでは、複数のスレッドが同時に走るため、処理の完了を互いに待機する必要があります。

thread::joinは、特定のスレッドが終了するまで、他のスレッドがその完了を待機するのに使用されます。

#include <iostream>
#include <thread>

void task1() {
    // 何らかの処理
    std::cout << "task1 finished\n";
}

void task2() {
    // 何らかの処理
    std::cout << "task2 finished\n";
}

int main() {
    std::thread t1(task1);
    std::thread t2(task2);

    t1.join();
    t2.join();

    std::cout << "Both tasks finished\n";
    return 0;
}

この例では、task1task2という2つのタスクを別々のスレッドで実行し、main関数ではjoinを使ってこれらのタスクが終了するのを待機しています。

○サンプルコード2:リソース共有の制御

マルチスレッドプログラムでは、複数のスレッドが同じリソースにアクセスする際に競合を避けるために、リソースの共有を制御することが重要です。

thread::joinを使用することで、リソースに対するアクセスが完了するまで他のスレッドの実行を待機させることができます。

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

int main() {
    int shared_resource = 0;

    auto increment = [&shared_resource]() {
        for (int i = 0; i < 10000; ++i) {
            ++shared_resource;
        }
    };

    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.push_back(std::thread(increment));
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final value of shared resource: " << shared_resource << '\n';
    return 0;
}

このコードでは、共有リソース(この場合は整数変数)に対して、複数のスレッドがインクリメント操作を行います。

joinを使用して全スレッドの処理が終わるのを待機しています。

○サンプルコード3:データの整合性保持

データの整合性を保つためには、スレッド間でのデータの同期が必要です。

thread::joinを用いることで、特定のデータに対するすべての操作が終了するまでプログラムの進行を待機させることができます。

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

std::mutex mtx; // ミューテックス

void safe_increment(int& value) {
    std::lock_guard<std::mutex> lock(mtx); // ミューテックスでロック
    ++value; // 安全なインクリメント
}

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

    for (int i = 0; i < 100;

 ++i) {
        threads.push_back(std::thread(safe_increment, std::ref(counter)));
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Counter: " << counter << '\n';
    return 0;
}

この例では、共有データ(counter)に対して複数のスレッドが安全にアクセスできるように、ミューテックスを使用しています。

各スレッドはsafe_increment関数を実行し、その完了をjoinで待機します。

○サンプルコード4:パフォーマンスの最適化

マルチスレッドプログラミングにおいてパフォーマンスの最適化は重要な課題です。

thread::joinを適切に使用することで、リソースの無駄遣いを防ぎ、全体の処理速度を向上させることが可能です。

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

void performTask(int id) {
    // 重い処理をシミュレーション
    std::this_thread::sleep_for(std::chrono::milliseconds(100 * id));
    std::cout << "Task " << id << " completed.\n";
}

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

    for (int i = 1; i <= 5; ++i) {
        threads.emplace_back(performTask, i);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << "All tasks completed.\n";
    return 0;
}

このコードでは、5つの異なるタスクを並列に実行し、各タスクが完了するのをjoinで待機します。

これにより、全てのタスクが効率的に処理され、プログラム全体のパフォーマンスが最適化されます。

○サンプルコード5:エラー処理と例外安全性

スレッドプログラミングにおけるエラー処理と例外安全性も、重要な側面です。

thread::joinを利用して、例外が発生した場合にもリソースが適切に解放されるようにすることが重要です。

#include <iostream>
#include <thread>
#include <stdexcept>

void taskWithException() {
    throw std::runtime_error("Exception from thread");
}

int main() {
    std::thread t(taskWithException);

    try {
        t.join();
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << '\n';
    }

    std::cout << "Main thread continues.\n";
    return 0;
}

この例では、taskWithException関数内で例外を投げ、main関数内のjoin呼び出しでこの例外をキャッチします。

これにより、例外が発生してもプログラムがクラッシュせず、適切に処理を続けることができます。

●注意点と対処法

C++におけるスレッドプログラミングでは、いくつかの重要な注意点があります。

これらを無視すると、プログラムの不安定性やパフォーマンスの低下を招く可能性があります。

ここでは、スレッドの安全な終了方法、リソースリークの防止、そしてパフォーマンスの落とし穴について、その対処法を解説します。

○スレッドの安全な終了方法

スレッドを安全に終了させることは、リソースの適切な解放とプログラムの安定性に不可欠です。

thread::joinは、スレッドが完了するまで待機するために使用します。

スレッドが終了していない状態でプログラムが終了すると、未処理の例外やデータの破損などの問題が発生する可能性があります。

○リソースリークの防止

マルチスレッドプログラムでは、スレッド間でリソースを共有することが多いため、リソースリークに注意する必要があります。

特に、動的に確保したメモリやファイルハンドル、ネットワーク接続などは、適切に解放することが重要です。

std::shared_ptrstd::unique_ptrを使用することで、自動的にリソースを解放することができます。

○パフォーマンスの落とし穴

スレッドの過剰な使用は、コンテキストスイッチの増加や同期のコストによって、逆にパフォーマンスを低下させることがあります。

スレッドの数は、使用するハードウェアのコア数に合わせることが一般的です。

また、不必要な同期やロックの使用は避け、効率的なデータ構造やアルゴリズムを選択することで、パフォーマンスを最適化できます。

●カスタマイズ方法

C++におけるスレッドプログラミングは、基本的な使用法を超えて、さまざまなカスタマイズが可能です。

ここでは、スレッドの優先順位の設定とカスタムスレッドプールの作成という二つのカスタマイズ方法を紹介します。

○スレッドの優先順位設定

スレッドの優先順位を設定することで、特定のタスクにより多くのCPU時間を割り当てることができます。

ただし、C++の標準ライブラリでは直接的な優先順位の設定はサポートされていません。

これを行うためには、プラットフォーム固有のAPIを使用する必要があります。

例えば、Windowsでは SetThreadPriority 関数を使い、Unix系のシステムでは pthread_setschedparam 関数を使用します。

○カスタムスレッドプールの作成

スレッドプールは、予め決められた数のスレッドを作成し、複数のタスクを効率的に処理するための手法です。

C++では、スレッドプールを手動で実装するか、サードパーティのライブラリを使用することでスレッドプールを作成できます。

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

class ThreadPool {
public:
    ThreadPool(size_t threads) : stop(false) {
        for(size_t i = 0; i < threads; ++i)
            workers.emplace_back(
                [this] {
                    for(;;) {
                        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;
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;

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

void exampleTask() {
    std::cout << "Processing task.\n";
}

int main() {
    ThreadPool pool(4);
    pool.enqueue(exampleTask);
    pool.enqueue(exampleTask);
    // タスクの追加と実行
    return 0;
}

このスレッドプールは、指定された数のスレッドを生成し、それらを使用してタスクを非同期に実行します。

スレッドプールは、複数のタスクを効率的に処理し、スレッドの生成と破棄のコストを削減するのに役立ちます。

まとめ

この記事では、C++におけるスレッド管理の基礎から応用、さらにはカスタマイズ方法までを詳細に解説しました。

thread::joinを中心に、スレッドの同期、リソース共有、データの整合性維持、パフォーマンスの最適化、エラー処理と例外安全性など、マルチスレッドプログラミングにおける重要な側面をカバーしています。

これらの知識を活用することで、初心者から上級者まで、C++のスレッド管理を理解し実践する上で非常に役立つでしょう。