C++ マルチスレッド

スレッドはプログラム内の軽量な実行単位であり、プログラムが複数のタスクを同時に実行することを可能にします。

マルチスレッドはマルチタスク処理の特殊な形式であり、マルチタスク処理はコンピュータに2つ以上のプログラムを同時に実行させることを可能にします。

一般に、2種類のマルチタスク処理があります:プロセスベースとスレッドベース。

  • プロセスベースのマルチタスク処理は、プログラムの並行実行です。
  • スレッドベースのマルチタスク処理は、同じプログラムの断片の並行実行です。

C++ マルチスレッドプログラミングは、1つのプログラム内で複数の並行実行スレッドを作成および管理することに関わります。

C++ は強力なマルチスレッドサポートを提供します。特に C++11 標準以降では、<thread>標準ライブラリにより、マルチスレッドプログラミングがより簡単かつ安全になりました。


概念の説明

スレッド (Thread)

  • スレッドはプログラム実行における単一の順次制御フローであり、複数のスレッドが同じプロセス内で独立して実行できます。
  • スレッドはプロセスのアドレス空間、ファイルディスクリプタ、ヒープ、グローバル変数などのリソースを共有しますが、各スレッドは独自のスタック、レジスタ、プログラムカウンタを持ちます。

並行処理 (Concurrency) と並列処理 (Parallelism)

  • 並行処理:複数のタスクがタイムスライス内で交互に実行され、同時に進行しているような効果を示します。
  • 並列処理:複数のタスクが複数のプロセッサまたはプロセッサコア上で同時に実行されます。

C++11 以降の標準はマルチスレッドサポートを提供し、中核コンポーネントは次のとおりです:

  • std::thread:スレッドの作成と管理に使用されます。
  • std::mutex:スレッド間の相互排他に使用され、複数のスレッドが共有リソースに同時にアクセスするのを防ぎます。
  • std::lock_guardとstd::unique_lock:ロックの取得と解放の管理に使用されます。
  • std::condition_variable:スレッド間の条件変数に使用され、スレッド間の待機と通知を調整します。
  • std::futureとstd::promise:スレッド間の値の受け渡しとタスク同期を実装するために使用されます。

スレッドの作成

C++11 以降、新しい標準スレッドライブラリが追加されました。std::thread,std::thread <thread>ヘッダファイルで宣言されています。したがって、std::threadを使用する場合は、<thread>ヘッダファイルをインクルードする必要があります。

注意:以前の一部のコンパイラでは C++11 のコンパイルパラメータは-std=c++11:

g++ -std=c++11 test.cpp 

std::thread

#include<thread>
std::thread thread_object(callable, args...);
  • callable:呼び出し可能オブジェクト。関数ポインタ、関数オブジェクト、Lambda 式などです。
  • args...:に渡されるcallable引数リストです。

関数ポインタを使用

関数ポインタを使ってスレッドを作成します。これは最も基本的な方法です:

例

#include <iostream>
#include <thread>

void printMessage(int count) {
    for (int i = 0; i < count; ++i) {
        std::cout << "Hello from thread (function pointer)!\n";
    }
}

int main() {
    std::thread t1(printMessage, 5); // スレッドを作成し、関数ポインタと引数を渡す
    t1.join(); // スレッドの完了を待つ
    return 0;
}

を使用してg++ -std=c++11コンパイルして実行すると、出力結果は次のようになります:

Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!

関数オブジェクトを使用

クラス内のoperator()operator() メソッドで関数オブジェクトを定義してスレッドを作成します:

例

#include <iostream>
#include <thread>

class PrintTask {
public:
    void operator()(int count) const {
        for (int i = 0; i < count; ++i) {
            std::cout << "Hello from thread (function object)!\n";
        }
    }
};

int main() {
    std::thread t2(PrintTask(), 5); // スレッドを作成し、関数オブジェクトと引数を渡す
    t2.join(); // スレッドの完了を待つ
    return 0;
}

を使用してg++ -std=c++11コンパイルして実行すると、出力結果は次のようになります:

Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!

Lambda 式を使用

Lambda 式を使用すると、スレッドが実行するコードを直接インラインで定義できます:

例

#include <iostream>
#include <thread>

int main() {
    std::thread t3([](int count) {
        for (int i = 0; i < count; ++i) {
            std::cout << "Hello from thread (lambda)!\n";
        }
    }, 5); // スレッドを作成し、Lambda 式と引数を渡す
    t3.join(); // スレッドの完了を待つ
    return 0;
}

を使用してg++ -std=c++11コンパイルして実行すると、出力結果は次のようになります:

Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!

スレッド管理

join()

join() はスレッドの実行が完了するまで待機するために使用します。join() または detach() を呼び出さずにスレッドオブジェクトを破棄すると、プログラムはクラッシュします。

t.join();

detach()

detach() はスレッドをメインスレッドから分離し、スレッドはバックグラウンドで独立して実行され、メインスレッドはそれを待たなくなります。

t.detach();

スレッドへの引数渡し

値渡し

引数は値でスレッドに渡すことができます:

std::thread t(func, arg1, arg2);

参照渡し

参照引数を渡す必要がある場合は、std::ref を使用する必要があります:

例

#include <iostream>
#include <thread>

void increment(int& x) {
    ++x;
}

int main() {
    int num = 0;
    std::thread t(increment, std::ref(num)); // std::ref を使用して参照を渡す
    t.join();
    std::cout << "Value after increment: " << num << std::endl;
    return 0;
}

総合例

以下は、上記の3つの方法を使用してスレッドを作成し、スレッド管理を行う完全な例です。

例

#include <iostream>
#include <thread>
using namespace std;

// スレッドのエントリポイントとして機能する単純な関数
void foo(int Z) {
    for (int i = 0; i < Z; i++) {
        cout << "スレッドは関数ポインタを呼び出し可能な引数として使用します\n";
    }
}

// 呼び出し可能オブジェクトのクラス定義
class ThreadObj {
public:
    void operator()(int x) const {
        for (int i = 0; i < x; i++) {
            cout << "スレッドは関数オブジェクトを呼び出し可能な引数として使用します\n";
        }
    }
};

int main() {
    cout << "スレッド 1、2、3 は独立して実行されます" << endl;

    // 関数ポインタを使用してスレッドを作成
    thread th1(foo, 3);

    // 関数オブジェクトを使用してスレッドを作成
    thread th2(ThreadObj(), 3);

    // Lambda 式を使用してスレッドを作成
    thread th3([](int x) {
        for (int i = 0; i < x; i++) {
            cout << "スレッドは lambda 式を呼び出し可能な引数として使用します\n";
        }
    }, 3);

    // すべてのスレッドの完了を待つ
    th1.join(); // スレッド th1 の完了を待つ
    th2.join(); // スレッド th2 の完了を待つ
    th3.join(); // スレッド th3 の完了を待つ

    return 0;
}

C++11 のコンパイルパラメータを使用-std=c++11:

g++ -std=c++11 test.cpp 

上記のコードの出力結果は、プラットフォームや実行ごとに異なる場合があります。スレッドの実行順序はオペレーティングシステムのスケジューリングアルゴリズムによって決まり、複数のスレッドが並行して実行されるため、出力が交錯する可能性があります。例:

线程 1 、2 、3 独立运行
线程使用函数指针作为可调用参数
线程使用函数对象作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用函数指针作为可调用参数
...

スレッド同期と相互排他

マルチスレッドプログラミングにおいて、スレッド同期と相互排他は非常に重要な2つの概念です。これらは、複数のスレッドによる共有リソースへのアクセスを制御し、データ競合やデッドロックなどの問題を回避するために使用されます。

1. ミューテックス(Mutex)

ミューテックスは、複数のスレッドが同時に共有リソースにアクセスするのを防ぐための同期プリミティブです。スレッドが共有リソースにアクセスする必要がある場合、まずミューテックスをロック(lock)する必要があります。ミューテックスが他のスレッドによってすでにロックされている場合、ロックを要求したスレッドは、ミューテックスがアンロック(unlock)されるまでブロックされます。

std::mutex:共有リソースを保護し、データ競合を防ぐために使用されます。

std::mutex mtx;
mtx.lock();   // 锁定互斥锁
// 访问共享资源
mtx.unlock(); // 释放互斥锁

std::lock_guardとstd::unique_lock:ロックの取得と解放を自動的に管理します。

std::lock_guard<std::mutex> lock(mtx); // 自动锁定和解锁
// 访问共享资源

ミューテックスの使用例:

例

#include <mutex>

std::mutex mtx; // グローバルミューテックス

void safeFunction() {
    mtx.lock(); // ミューテックスのロックを要求
    // 共有リソースにアクセスまたは変更
    mtx.unlock(); // ミューテックスを解放
}

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

2. ロック(Locks)

C++ は、ミューテックスの使用と管理を簡素化するための複数のロックタイプを提供します。

一般的なロックタイプは次のとおりです:

  • std::lock_guard:スコープロック。構築時にミューテックスを自動的にロックし、破棄時に自動的にアンロックします。
  • std::unique_lock:std::lock_guard に似ていますが、所有権の移動や手動アンロックなど、より柔軟性があります。

ロックの使用例:

例

#include <mutex>

std::mutex mtx;

void safeFunctionWithLockGuard() {
    std::lock_guard<std::mutex> lk(mtx);
    // 共有リソースにアクセスまたは変更
}

void safeFunctionWithUniqueLock() {
    std::unique_lock<std::mutex> ul(mtx);
    // 共有リソースにアクセスまたは変更
    // ul.unlock(); // オプション:手動アンロック
    // ...
}

3. 条件変数(Condition Variable)

条件変数はスレッド間の調整に使用され、1つ以上のスレッドが特定の条件の発生を待つことを可能にします。通常、ミューテックスと一緒に使用され、スレッド間の同期を実現します。

std::condition_variableスレッド間の待機と通知メカニズムを実装するために使用されます。

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

std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待条件满足
// 条件满足后执行

条件変数の使用例:

例

#include <mutex>
#include <condition_variable>

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

void workerThread() {
    std::unique_lock<std::mutex> lk(mtx);
    cv.wait(lk, []{ return ready; }); // 条件を待つ
    // 条件が満たされたときに処理を実行する
}

void mainThread() {
    {
        std::lock_guard<std::mutex> lk(mtx);
        // データを準備する
        ready = true;
    } // スコープを離れるときにロックを解除する
    cv.notify_one(); // 待機中のスレッドに通知する
}

4. 原子操作(Atomic Operations)

原子操作は共有データへのアクセスが不可分であることを保証します。つまり、マルチスレッド環境では、原子操作は完全に実行されるか、まったく実行されないかのどちらかであり、中間状態は発生しません。

原子操作の使用例:

例

#include <atomic>
#include <thread>

std::atomic<int> count(0);

void increment() {
    count.fetch_add(1, std::memory_order_relaxed);
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    return count; // 2を返すべき
}

5. スレッド局所記憶(Thread Local Storage, TLS)

スレッドローカルストレージにより、各スレッドが独自のデータコピーを持つことができます。これはthread_localキーワードを使用して実現でき、共有リソースへの競合を回避できます。

スレッドローカルストレージの使用例:

例

#include <iostream>
#include <thread>

thread_local int threadData = 0;

void threadFunction() {
    threadData = 42; // 各スレッドは独自のthreadDataコピーを持つ
    std::cout << "Thread data: " << threadData << std::endl;
}

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

6. デッドロック(Deadlock)と回避戦略

デッドロックは、複数のスレッドが互いに相手のリソース解放を待機し合い、どのスレッドも実行を継続できない状態で発生します。デッドロックを回避する戦略には以下のものがあります:

  • 常に同じ順序でリソースを要求する。
  • タイムアウトを使用してリソースの取得を試みる。
  • デッドロック検出アルゴリズムを使用する。

スレッド間通信

std::future と std::promise:スレッド間の値の受け渡しを実現します。

例


std::promise<int> p;
std::future<int> f = p.get_future();

std::thread t([&p] {
    p.set_value(10); // 値を設定し、futureをトリガーする
});

int result = f.get(); // 値を取得する

メッセージキュー(std::queue と std::mutex に基づく)により、簡単なスレッド間通信を実現します。


C++17 は並列アルゴリズムライブラリ(<algorithm>)を導入しました。その一部のアルゴリズムは並列実行をサポートしており、マルチコア CPU を利用してパフォーマンスを向上させることができます。

例

#include <algorithm>
#include <vector>
#include <execution>

std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(std::execution::par, vec.begin(), vec.end(), [](int &n) {
    n *= 2;
});

C++ 標準ライブラリの並列アルゴリズム

その他の例:

C++ マルチスレッド:http://www.example.com/w3cnote/cpp-multithread-demo.html

C++ std::thread: https://www.example.com/w3cnote/cpp-std-thread.html

その他の拡張