見出し画像

C++とマルチスレッディング

マルチスレッディング
マルチスレッディングは、特にC++の文脈でコンピュータプログラミングの世界での基本的な概念です。その核心には、単一のプロセス内で複数のスレッドが同時に実行されるという要素が含まれます。スレッドは最小の実行単位であり、プログラムが複数のタスクを同時に実行できるようにします。プログラムを単一の、一枚岩の実体として実行するのではなく、マルチスレッディングは、より小さな、より管理しやすいスレッドに分割して独立して実行できるようにします。この並行実行は、アプリケーションの性能と応答性を大幅に向上させることができます。
マルチスレッディングは、複数のスレッドが同時に実行されることで、マルチコアプロセッサの効率的な利用を可能にし、CPUの使用率を向上させることができます。ただし、効果的なマルチスレッディングを実現するには、適切な設計と同期が必要で、利点を最大限に活用し、潜在的な問題を最小限に抑える必要があります。CPUの使用率を最適化するために、アプリケーションの要件と対象のハードウェアの特性を慎重に考慮することが重要です。

スレッドとは何か?
マルチスレッディングの文脈において、「スレッド」はプロセス内の最小の実行単位を指します。スレッドは、基本的には親プロセスと同じメモリ空間とリソースを共有しながらも独立して実行できる軽量なプロセスです。各スレッドは独自のプログラムカウンター、スタック、およびレジスタのセットを持ち、他のスレッドと同時にコードを実行できます。スレッドは「ワーカー」と呼ばれ、並列でさまざまなタスクを実行し、マルチコアプロセッサを最大限に活用します。

 プロセスとスレッド
プロセスとスレッドはどちらも実行単位ですが、いくつかの重要な点で異なります:

プロセス:プロセスは独立して自己完結型のプログラムで、独自のメモリ空間で実行されます。プロセスはメモリやリソースを直接共有しないため、それら間の通信はより複雑で遅くなります。各プロセスは独自のアドレススペースを持ち、他のプロセスから隔離されています。

スレッド:スレッドはプロセス内の実行単位であり、同じメモリ空間を共有します。スレッドは共有データ、変数、およびリソースに直接アクセスすることで、より簡単に通信できます。スレッドはプロセスよりも軽量で効率的ですが、同じレベルの隔離を持たないため、データ競合などの同時実行の問題に対して脆弱です。

 シングルスレッド対マルチスレッドのプログラム

シングルスレッドプログラム:シングルスレッドプログラムでは、実行経路が1つだけ存在します。つまり、タスクは順次実行されます。シングルスレッドプログラムは、マルチコアプロセッサの完全な活用を行わず、時間のかかるタスクを実行する際に非効率的になる可能性があります。

マルチスレッドプログラム:マルチスレッドプログラムは、複数のスレッドを使用してタスクを並列に実行します。これにより、性能、応答性、および利用可能なCPUコアの効率的な利用が大幅に向上する可能性があります。

それでは、C++のスレッドライブラリについて詳しく説明してみましょう。C++11から、スレッドが利用可能となっています。

<thread>
ヘッダ
threadヘッダは、threadクラスとthis_thread名前空間を宣言します。

std::thread
class thread;
クラス
個々の実行スレッドを表すクラス。
実行スレッドは、マルチスレッディング環境で他の実行スレッドと同じアドレス空間を共有しながら、並行に実行できる命令のシーケンスです。

std::thread::thread
thread()

コンストラクタ
スレッドオブジェクトを構築します。

std::thread::~thread
~thread();

デストラクタ
スレッドオブジェクトを破棄します。

スレッドのコンストラクタとデストラクタについて、例を交えて説明します。
スレッドのコンストラクタ:
例:

explicit thread(Function &&f, Args &&... args);
  • Function は実行されるコードを表す呼び出し可能オブジェクトで、関数、ファンクタ、またはラムダなどが含まれます。

  • args は関数が呼び出される際に渡す引数です。

スレッドのデストラクタ:

std::thread クラスのデストラクタは、スレッドに関連するリソースの解放を担当します。スレッドオブジェクトがスコープを抜けると、スレッドが結合可能であり、かつ結合またはデタッチされていない場合、std::terminate() を呼び出します。したがって、スレッドがデストラクトされる前に、スレッドを結合またはデタッチすることを確実にすることが重要です。

例:

#include <iostream>
#include <thread>

void threadFunction(int id) 
{
    std::cout << "Thread " << id << " is running." << std::endl;
}

int main() 
{
    std::thread t1(threadFunction, 1);  // スレッドt1を作成および開始

    // t1が結合可能かどうかを確認
    if (t1.joinable())
    {
        std::cout << "Thread 1 is joinable." << std::endl;
    }

    // スレッドの終了を待つ(結合)
    t1.join();

    // 結合後にt1が結合可能かどうかを再確認
    if (!t1.joinable()) 
    {
        std::cout << "Thread 1 is no longer joinable." << std::endl;
    }

    return 0;
}

この例では:

  1. 関数 threadFunction を使用して、スレッド t1 を作成し、引数として threadFunction と識別子 1 を渡します。

  2. joinable() 関数を使用して、t1 が結合可能な状態であるかどうかを確認します。

  3. join() メソッドを使用して、t1 が実行を完了するのを待ちます。

  4. 結合後に、再び t1 が結合可能でないかどうかを確認します。これで、リソースが解放されたことが示されます。

この例では、スレッドのコンストラクタとデストラクタの使用法を示し、std::thread オブジェクトがスコープを抜けるときに正しい動作を確保するためにスレッドの状態を適切に管理する重要性を強調しています。

threadクラスのメンバー関数

1. thread::native_handle : native_handle関数は、プラットフォーム固有のスレッドハンドルへのアクセスを提供する関数です。これはC++標準ライブラリの一部ではなく、プラットフォームに依存します。これは、標準のC++スレッドインターフェースでは提供されないプラットフォーム固有のスレッディング機能を扱う必要がある場合に主に使用されます。

2. thread::detach : detach 関数は、スレッドをデタッチするために使用されます。スレッドをデタッチすると、メインスレッドが終了してもスレッドは独立して実行し続けます。これは、スレッドの終了を待つ必要がない場合に有用です。

3. thread::join : join 関数は、スレッドの実行が完了するのを待つために使用されます。この関数は呼び出し元のスレッドをブロックし、指定されたスレッドが作業を完了するまで待機します。これは、スレッド間の同期に重要です。

4. thread::joinable : joinable 関数は、スレッドがジョイン可能かどうかを確認します。つまり、スレッドがまだ実行中であるか、detachされていない場合は true を返し、joinできない場合(detachされているか、既にjoinされている場合)は false を返します。

5. thread::swap : swap 関数は、2 つの std::thread オブジェクトの状態を交換できます。つまり、swapが表すスレッドの役割を交換できます。

6. thread::get_id : get_id 関数は、スレッドの一意の識別子を返します。同じ関数を実行する複数のスレッドを区別するために使用できます。

例:

#include <iostream>
#include <thread>

void myFunction(int id) 
{
    std::cout << "スレッドID: " << std::this_thread::get_id() << "、関数ID: " << id << std::endl;
}

int main() 
{
    // スレッド1を作成し、myFunction関数を実行するように指定
    std::thread thread1(myFunction, 1);

    // スレッド2を作成し、myFunction関数を実行するように指定
    std::thread thread2(myFunction, 2);

    // スレッド1がジョイン可能かチェックし、ジョインする
    if (thread1.joinable()) 
    {
        thread1.join();
        std::cout << "スレッド1がジョインされました。" << std::endl;
    }

    // スレッド2がジョイン可能かチェックし、デタッチする
    if (thread2.joinable()) 
    {
        thread2.detach();
        std::cout << "スレッド2がデタッチされました。" << std::endl;
    }

    // スレッド1とスレッド2の状態を交換
    thread1.swap(thread2);

    // スレッド1がジョイン可能か再度チェックし、ジョインする
    if (thread1.joinable()) 
    {
        thread1.join();
        std::cout << "スレッド1がジョインされました。" << std::endl;
    }

    return 0;
}

この例では:

  1. myFunction 関数は、スレッドが実行する関数です。この関数は2つの引数を受け取り、それぞれスレッドのIDと関数のIDを表示します。

  2. main 関数内では、2つのスレッドを作成します。thread1 と thread2 はそれぞれ myFunction 関数を実行するように設定されています。

  3. if (thread1.joinable()) と if (thread2.joinable()) は、各スレッドがジョイン可能かどうかをチェックしています。ジョイン可能なスレッドは join() で待つことができます。thread1 はジョインされ、thread2 はデタッチされます。

  4. thread1.swap(thread2) は、スレッド thread1 と thread2 の状態を交換します。これにより、それぞれのスレッドが異なる役割を持つことになります。

  5. 最後に、再び thread1 がジョイン可能かどうかをチェックして、ジョインされます。

この記事では、C++におけるマルチスレッディングの概念とスレッドライブラリの役割について探求しました。この記事が、マルチスレッディングの基本的な理解を提供し、貴重な並行プログラミングの概念のさらなる探求と実用的な活用の出発点となることを願っています。

                                                 エンジニアファーストの会社 株式会社CRE-CO
                                                                                                    su_myat_phyu

参考
https://cplusplus.com/
cppreference.com
cpprefjp - C++日本語リファレンス : https://cpprefjp.github.io/


この記事が気に入ったらサポートをしてみませんか?