C++のラムダ式とマルチスレッド環境でのデータ共有方法

C++のラムダ式とマルチスレッド環境でのデータ共有は、現代のソフトウェア開発において非常に重要な技術です。ラムダ式は、簡潔なコードで匿名関数を記述するための機能であり、特にコールバックやスレッド処理でよく使われます。一方、マルチスレッドプログラミングは、プログラムのパフォーマンスを向上させるために、複数のスレッドを利用して並列処理を行う技術です。しかし、複数のスレッド間でデータを共有することは、データ競合やデッドロックなどの問題を引き起こす可能性があります。本記事では、C++のラムダ式の基本から始め、マルチスレッド環境でのデータ共有の具体的な方法とその課題解決策について、実践的な例を交えて詳しく解説します。これにより、より安全で効率的なマルチスレッドプログラミングの実現を目指します。

目次
  1. ラムダ式の基本
    1. ラムダ式の基本構文
    2. 基本例
    3. 省略可能な部分
    4. ラムダ式のキャプチャリスト
  2. ラムダ式のキャプチャリスト
    1. キャプチャリストの基本
    2. キャプチャの方法
  3. スレッドの基本
    1. スレッドの作成と実行
    2. スレッド関数の引数
    3. ラムダ式とスレッド
    4. スレッドの分離
    5. スレッドの終了とリソース管理
  4. スレッドとデータ共有の問題点
    1. データ競合(Race Condition)
    2. デッドロック(Deadlock)
    3. ライブロック(Livelock)
    4. データの一貫性の確保
  5. ミューテックスによるデータ共有の解決方法
    1. ミューテックスの基本的な使い方
    2. デッドロックの回避
    3. スピンロックの使用
    4. 再帰的ミューテックス
  6. ラムダ式とスレッドの連携
    1. ラムダ式を使ったスレッドの作成
    2. キャプチャリストによる変数の共有
    3. スレッド間のデータ同期
  7. 条件変数の使用
    1. 条件変数の基本的な使い方
    2. タイムアウトを使用した待機
    3. 生産者-消費者問題の実装
  8. 実践例:マルチスレッド環境でのデータ共有
    1. 準備:必要なヘッダとグローバル変数の定義
    2. スレッド関数の定義
    3. メイン関数の定義
    4. コード全体の例
  9. 応用例:複雑なデータ構造の共有
    1. 準備:必要なヘッダとグローバル変数の定義
    2. データ追加関数の定義
    3. データ処理関数の定義
    4. メイン関数の定義
    5. コード全体の例
  10. 演習問題
    1. 問題1: 複数スレッドでのカウンタインクリメント
    2. 問題2: スレッド間での文字列共有
    3. 問題3: 条件変数を使ったスレッド同期
  11. まとめ

ラムダ式の基本

ラムダ式は、C++11で導入された匿名関数の一種であり、関数オブジェクトを簡潔に定義するための強力な手段です。ラムダ式は、通常の関数と同様に引数を取り、関数本体で処理を行い、結果を返すことができます。

ラムダ式の基本構文

C++のラムダ式は、以下のような構文で記述されます。

[capture](parameters) -> return_type {
    // function body
};
  • capture: ラムダ式が関数外の変数を使用するために必要なキャプチャリスト
  • parameters: 関数の引数
  • return_type: 関数の戻り値の型
  • function body: 関数の処理内容

基本例

簡単なラムダ式の例を見てみましょう。

#include <iostream>

int main() {
    auto add = [](int a, int b) -> int {
        return a + b;
    };

    std::cout << "Sum: " << add(3, 4) << std::endl;
    return 0;
}

この例では、addというラムダ式を定義し、二つの整数を受け取り、その和を返しています。

省略可能な部分

多くの場合、ラムダ式の構文は簡略化できます。例えば、戻り値の型はコンパイラが自動推論できるため、省略可能です。また、引数リストが空の場合、()も省略できます。

#include <iostream>

int main() {
    auto greet = [] {
        std::cout << "Hello, World!" << std::endl;
    };

    greet();
    return 0;
}

この例では、引数も戻り値もないラムダ式を定義しています。

ラムダ式のキャプチャリスト

キャプチャリストを使うことで、ラムダ式の外部にある変数を関数内で使用できます。以下にキャプチャリストの基本的な使い方を示します。

#include <iostream>

int main() {
    int x = 10;
    auto printX = [x] {
        std::cout << "x = " << x << std::endl;
    };

    printX();
    return 0;
}

この例では、xをキャプチャリストに含めることで、ラムダ式内でxの値を使用しています。

ラムダ式は、短くて読みやすいコードを記述するための強力なツールです。次のセクションでは、ラムダ式のキャプチャリストについてさらに詳しく見ていきます。

ラムダ式のキャプチャリスト

ラムダ式のキャプチャリストを使うことで、ラムダ式外部の変数を関数内で利用することができます。キャプチャリストには、変数を値渡し(コピー)する方法と参照渡しする方法があります。

キャプチャリストの基本

キャプチャリストは、[]の中にキャプチャしたい変数を指定します。以下にキャプチャリストの基本的な使い方を示します。

値渡し(コピー)

変数を値渡しでキャプチャする場合、キャプチャリストに変数名を記述します。この方法では、ラムダ式内部で使用する変数は、外部の変数のコピーとなります。

#include <iostream>

int main() {
    int x = 10;
    auto printX = [x] {
        std::cout << "x = " << x << std::endl;
    };

    x = 20;
    printX(); // 出力: x = 10
    return 0;
}

この例では、xを値渡しでキャプチャしているため、ラムダ式内のxは外部のxとは異なるコピーです。

参照渡し

変数を参照渡しでキャプチャする場合、キャプチャリストに&を付けて変数名を記述します。この方法では、ラムダ式内部で使用する変数は、外部の変数そのものとなります。

#include <iostream>

int main() {
    int x = 10;
    auto printX = [&x] {
        std::cout << "x = " << x << std::endl;
    };

    x = 20;
    printX(); // 出力: x = 20
    return 0;
}

この例では、xを参照渡しでキャプチャしているため、ラムダ式内のxは外部のxと同じ変数を指します。

キャプチャの方法

キャプチャリストでは、以下のような様々なキャプチャ方法が使えます。

すべての変数を値渡しでキャプチャ

キャプチャリストに=を使うと、ラムダ式外部のすべての変数を値渡しでキャプチャします。

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    auto printXY = [=] {
        std::cout << "x = " << x << ", y = " << y << std::endl;
    };

    printXY(); // 出力: x = 10, y = 20
    return 0;
}

すべての変数を参照渡しでキャプチャ

キャプチャリストに&を使うと、ラムダ式外部のすべての変数を参照渡しでキャプチャします。

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    auto printXY = [&] {
        std::cout << "x = " << x << ", y = " << y << std::endl;
    };

    x = 30;
    y = 40;
    printXY(); // 出力: x = 30, y = 40
    return 0;
}

特定の変数を個別にキャプチャ

キャプチャリストで個別に変数を指定してキャプチャすることもできます。

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    auto printXY = [x, &y] {
        std::cout << "x = " << x << ", y = " << y << std::endl;
    };

    x = 30;
    y = 40;
    printXY(); // 出力: x = 10, y = 40
    return 0;
}

この例では、xは値渡しで、yは参照渡しでキャプチャされています。

ラムダ式のキャプチャリストは、柔軟に外部の変数を取り扱うための強力なツールです。次のセクションでは、C++のスレッドの基本について説明します。

スレッドの基本

C++でのスレッドは、プログラムの並列処理を実現するための重要な機能です。スレッドを使うことで、複数の処理を同時に実行でき、プログラムのパフォーマンスを向上させることができます。ここでは、C++におけるスレッドの基本的な使用方法について説明します。

スレッドの作成と実行

C++11から導入された標準ライブラリの<thread>ヘッダを使うことで、簡単にスレッドを作成し、実行することができます。以下に基本的なスレッドの作成例を示します。

#include <iostream>
#include <thread>

void printHello() {
    std::cout << "Hello from thread!" << std::endl;
}

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

この例では、printHello関数を実行する新しいスレッドを作成し、t.join()でスレッドの終了を待っています。

スレッド関数の引数

スレッドに引数を渡すこともできます。以下に引数付きのスレッド関数の例を示します。

#include <iostream>
#include <thread>

void printNumber(int num) {
    std::cout << "Number: " << num << std::endl;
}

int main() {
    std::thread t(printNumber, 42);
    t.join(); // スレッドの終了を待つ
    return 0;
}

この例では、printNumber関数に整数引数を渡してスレッドを作成しています。

ラムダ式とスレッド

ラムダ式を使ってスレッドを作成することもできます。これにより、匿名関数をその場で定義し、実行することができます。

#include <iostream>
#include <thread>

int main() {
    std::thread t([]{
        std::cout << "Hello from lambda!" << std::endl;
    });
    t.join(); // スレッドの終了を待つ
    return 0;
}

この例では、匿名関数を使ってスレッドを作成し、同じくjoinでスレッドの終了を待っています。

スレッドの分離

joinメソッドの代わりにdetachメソッドを使うと、スレッドを分離してメインスレッドから独立して実行させることができます。

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

void printMessage() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Hello from detached thread!" << std::endl;
}

int main() {
    std::thread t(printMessage);
    t.detach(); // スレッドを分離
    std::cout << "Main thread continues..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2)); // メインスレッドを待機
    return 0;
}

この例では、detachメソッドを使ってスレッドを分離し、メインスレッドが終了する前にメッセージを出力するようにしています。

スレッドの終了とリソース管理

スレッドを作成した後、必ずjoinまたはdetachを呼び出してスレッドの終了を管理することが重要です。そうしないと、プログラム終了時にスレッドが存在していると未定義動作を引き起こす可能性があります。

C++のスレッドは強力なツールですが、適切な管理が必要です。次のセクションでは、スレッドとデータ共有の問題点について詳しく説明します。

スレッドとデータ共有の問題点

マルチスレッドプログラミングにおいて、複数のスレッド間でデータを共有することは避けて通れない課題です。しかし、データ共有にはいくつかの問題点があり、これらを正しく扱わないとプログラムが不安定になったり、意図しない動作を引き起こす可能性があります。ここでは、マルチスレッド環境でのデータ共有に関する主要な問題点を説明します。

データ競合(Race Condition)

データ競合は、複数のスレッドが同じデータに同時にアクセスしようとする際に発生する問題です。これは、スレッドがデータを読み取った後に他のスレッドがそのデータを書き換えると、最初のスレッドが期待していた結果が得られなくなることがあります。

#include <iostream>
#include <thread>

int counter = 0;

void increment() {
    for (int i = 0; i < 1000; ++i) {
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "Counter: " << counter << std::endl; // 期待値は2000だが、不定
    return 0;
}

この例では、counterが期待値の2000にならない可能性があります。これは、データ競合が発生しているためです。

デッドロック(Deadlock)

デッドロックは、複数のスレッドが互いにロックを待ち続ける状態で、プログラムが停止してしまう問題です。デッドロックは、ロックの順序が不適切な場合や、複数のロックを取得しようとする際に発生します。

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

std::mutex m1;
std::mutex m2;

void thread1() {
    std::lock_guard<std::mutex> lock1(m1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // シミュレーションのため
    std::lock_guard<std::mutex> lock2(m2);
    std::cout << "Thread 1 finished" << std::endl;
}

void thread2() {
    std::lock_guard<std::mutex> lock1(m2);
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // シミュレーションのため
    std::lock_guard<std::mutex> lock2(m1);
    std::cout << "Thread 2 finished" << std::endl;
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

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

    return 0;
}

この例では、m1m2のロックを取得する順序が逆になっているため、デッドロックが発生する可能性があります。

ライブロック(Livelock)

ライブロックは、スレッドが互いに譲り合いを続けることで、進行不能になる問題です。これは、デッドロックと異なり、スレッドが動作し続けているが、進展がない状態です。

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

std::mutex m;

void threadFunction(int& sharedData) {
    while (true) {
        if (m.try_lock()) {
            ++sharedData;
            m.unlock();
            break;
        }
    }
}

int main() {
    int data = 0;
    std::thread t1(threadFunction, std::ref(data));
    std::thread t2(threadFunction, std::ref(data));

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

    std::cout << "Data: " << data << std::endl;
    return 0;
}

この例では、スレッドがtry_lockを使ってロックを取得しようとし続けるため、ライブロックが発生する可能性があります。

データの一貫性の確保

データの一貫性を確保するためには、クリティカルセクションを適切に管理することが必要です。クリティカルセクションとは、同時に一つのスレッドだけが実行できるコードの部分を指します。これを管理するためには、ミューテックスやその他の同期プリミティブを使用します。

次のセクションでは、ミューテックスを使ってデータ共有の問題を解決する方法について詳しく説明します。

ミューテックスによるデータ共有の解決方法

マルチスレッド環境でのデータ共有の問題を解決するために、ミューテックス(mutex)を使用することが一般的です。ミューテックスは、同時に一つのスレッドだけが共有データにアクセスできるようにするための排他制御機構です。ここでは、ミューテックスを使ってデータ競合やデッドロックを回避する方法について説明します。

ミューテックスの基本的な使い方

C++の標準ライブラリで提供されているstd::mutexを使用して、クリティカルセクションを保護する方法を見ていきましょう。

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

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "Counter: " << counter << std::endl; // 出力: Counter: 2000
    return 0;
}

この例では、std::lock_guardを使ってミューテックスを自動的にロックおよびアンロックしています。std::lock_guardはスコープを抜けると自動的にミューテックスを解放するため、手動でアンロックする必要がありません。

デッドロックの回避

デッドロックを回避するためには、ロックの順序を統一することが重要です。複数のミューテックスを使う場合、常に同じ順序でロックを取得することでデッドロックを防ぐことができます。

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

std::mutex m1;
std::mutex m2;

void thread1() {
    std::lock(m1, m2);
    std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
    std::cout << "Thread 1 finished" << std::endl;
}

void thread2() {
    std::lock(m1, m2);
    std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
    std::cout << "Thread 2 finished" << std::endl;
}

int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);

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

    return 0;
}

この例では、std::lockを使って同時に複数のミューテックスをロックし、その後でstd::lock_guardstd::adopt_lockとともに使用してデッドロックを回避しています。

スピンロックの使用

軽量なロックメカニズムとして、スピンロックを使用することもできます。スピンロックは、ロックが取得できるまで繰り返し試行するため、コンテキストスイッチが少ない状況で効果的です。

#include <iostream>
#include <thread>
#include <atomic>

std::atomic_flag lock = ATOMIC_FLAG_INIT;
int counter = 0;

void increment() {
    for (int i = 0; i < 1000; ++i) {
        while (lock.test_and_set(std::memory_order_acquire)); // ロック取得を試行
        ++counter;
        lock.clear(std::memory_order_release); // ロック解放
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "Counter: " << counter << std::endl; // 出力: Counter: 2000
    return 0;
}

この例では、std::atomic_flagを使ってスピンロックを実装しています。スピンロックは、短期間のロック取得には有効ですが、長期間のロック保持が必要な場合には適さないことがあります。

再帰的ミューテックス

再帰的ミューテックス(std::recursive_mutex)は、同じスレッドが複数回ロックを取得する必要がある場合に使用します。

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

std::recursive_mutex rmtx;
int counter = 0;

void increment() {
    rmtx.lock();
    ++counter;
    rmtx.lock();
    ++counter;
    rmtx.unlock();
    rmtx.unlock();
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "Counter: " << counter << std::endl; // 出力: Counter: 4
    return 0;
}

この例では、std::recursive_mutexを使って同じスレッド内で複数回ロックを取得しています。

ミューテックスを使うことで、マルチスレッド環境でのデータ共有の問題を効果的に解決できます。次のセクションでは、ラムダ式とスレッドの連携について詳しく説明します。

ラムダ式とスレッドの連携

ラムダ式は、その簡潔さと柔軟性から、マルチスレッドプログラミングで頻繁に使用されます。特に、スレッド関数としてラムダ式を使うことで、コードを簡潔かつ明瞭に記述できます。ここでは、ラムダ式を使ってスレッドを管理し、データ共有を効率化する方法を説明します。

ラムダ式を使ったスレッドの作成

ラムダ式を使ってスレッドを作成する例を見てみましょう。

#include <iostream>
#include <thread>

int main() {
    int counter = 0;
    std::mutex mtx;

    auto increment = [&counter, &mtx]() {
        for (int i = 0; i < 1000; ++i) {
            std::lock_guard<std::mutex> lock(mtx);
            ++counter;
        }
    };

    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "Counter: " << counter << std::endl; // 出力: Counter: 2000
    return 0;
}

この例では、ラムダ式を使ってincrement関数を定義し、その中でミューテックスを使用してデータ競合を防ぎながらcounterをインクリメントしています。

キャプチャリストによる変数の共有

ラムダ式のキャプチャリストを使うことで、外部の変数を共有できます。前述の例では、countermtxを参照渡しでキャプチャしています。

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

int main() {
    std::vector<int> data(10, 0);
    std::mutex mtx;

    auto processData = [&data, &mtx](int start, int end) {
        for (int i = start; i < end; ++i) {
            std::lock_guard<std::mutex> lock(mtx);
            data[i] += 1;
        }
    };

    std::thread t1(processData, 0, 5);
    std::thread t2(processData, 5, 10);

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

    for (const auto& val : data) {
        std::cout << val << " "; // 出力: 1 1 1 1 1 1 1 1 1 1
    }
    std::cout << std::endl;

    return 0;
}

この例では、ラムダ式を使ってベクターdataの一部を処理し、それぞれの要素をインクリメントしています。ミューテックスを使ってデータ競合を防いでいます。

スレッド間のデータ同期

ラムダ式を使ってスレッド間のデータ同期を行うこともできます。以下の例では、条件変数を使ってスレッド間の同期を実現しています。

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

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

void print_id(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; }); // readyがtrueになるまで待機
    std::cout << "Thread " << id << std::endl;
}

void set_ready() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_all(); // 全ての待機スレッドを起床
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i) {
        threads[i] = std::thread(print_id, i);
    }

    std::this_thread::sleep_for(std::chrono::seconds(1)); // メインスレッドを1秒待機
    set_ready(); // 全スレッドを起床

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

    return 0;
}

この例では、条件変数cvを使ってスレッド間の同期を行っています。set_ready関数が呼ばれると、すべてのスレッドが起床し、print_id関数を実行します。

ラムダ式とスレッドを組み合わせることで、効率的かつ簡潔なマルチスレッドプログラミングが可能になります。次のセクションでは、条件変数を使ったスレッド間の同期方法についてさらに詳しく説明します。

条件変数の使用

条件変数は、スレッド間の同期を取るための強力なツールです。特に、スレッドが特定の条件を満たすまで待機し、その条件が満たされたときに通知を受けて動作を再開する場合に有効です。ここでは、条件変数の基本的な使い方と応用方法について説明します。

条件変数の基本的な使い方

条件変数は、std::condition_variableクラスを使用して実装されます。条件変数は、ミューテックスと一緒に使用され、特定の条件が満たされるまでスレッドを待機させることができます。

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

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

void print_id(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; }); // readyがtrueになるまで待機
    std::cout << "Thread " << id << std::endl;
}

void set_ready() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_all(); // 全ての待機スレッドを起床
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i) {
        threads[i] = std::thread(print_id, i);
    }

    std::this_thread::sleep_for(std::chrono::seconds(1)); // メインスレッドを1秒待機
    set_ready(); // 全スレッドを起床

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

    return 0;
}

この例では、条件変数cvを使ってスレッド間の同期を取っています。print_id関数のスレッドは、readytrueになるまで待機し、set_ready関数がreadytrueに設定して条件変数に通知を送ります。

タイムアウトを使用した待機

条件変数は、指定した時間だけ待機し、時間が経過しても条件が満たされない場合に処理を進めることができます。

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

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

void wait_for_ready(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    if (cv.wait_for(lock, std::chrono::seconds(2), []{ return ready; })) {
        std::cout << "Thread " << id << " finished waiting" << std::endl;
    } else {
        std::cout << "Thread " << id << " timed out" << std::endl;
    }
}

int main() {
    std::thread t1(wait_for_ready, 1);

    std::this_thread::sleep_for(std::chrono::seconds(1)); // メインスレッドを1秒待機
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();

    t1.join();

    return 0;
}

この例では、wait_for_ready関数が条件変数cvを使って最大2秒間待機します。もしreadytrueになれば「finished waiting」を出力し、タイムアウトした場合は「timed out」を出力します。

生産者-消費者問題の実装

条件変数は、生産者-消費者問題のような典型的な同期問題の解決にも使用されます。以下の例では、生産者がデータを生成し、消費者がそのデータを処理するシナリオを示します。

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

std::mutex mtx;
std::condition_variable cv;
std::queue<int> dataQueue;
bool done = false;

void producer() {
    for (int i = 0; i < 10; ++i) {
        {
            std::lock_guard<std::mutex> lock(mtx);
            dataQueue.push(i);
        }
        cv.notify_one(); // データが追加されたことを通知
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    {
        std::lock_guard<std::mutex> lock(mtx);
        done = true;
    }
    cv.notify_all();
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return !dataQueue.empty() || done; }); // データが追加されるか、doneがtrueになるまで待機
        while (!dataQueue.empty()) {
            int data = dataQueue.front();
            dataQueue.pop();
            std::cout << "Consumed: " << data << std::endl;
        }
        if (done) break;
    }
}

int main() {
    std::thread prod(producer);
    std::thread cons(consumer);

    prod.join();
    cons.join();

    return 0;
}

この例では、producer関数がデータを生成してキューに追加し、consumer関数がキューからデータを取り出して処理します。条件変数cvを使って、データが生成されるのを待機し、処理が完了したらスレッドを終了します。

条件変数を使うことで、スレッド間の同期を効果的に管理できます。次のセクションでは、具体的なコード例を通じてマルチスレッド環境でのデータ共有方法を説明します。

実践例:マルチスレッド環境でのデータ共有

ここでは、実際のコード例を通じて、マルチスレッド環境でのデータ共有方法を具体的に説明します。この例では、スレッド間で整数のカウンタを共有し、そのカウンタをインクリメントする処理を行います。

準備:必要なヘッダとグローバル変数の定義

まず、必要なヘッダファイルをインクルードし、グローバル変数を定義します。

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

std::mutex mtx;
int counter = 0;

ここでは、std::mutexを使って排他制御を行い、共有データであるcounterを定義しています。

スレッド関数の定義

次に、スレッドで実行される関数を定義します。この関数では、カウンタをインクリメントし、その値を出力します。

void incrementCounter(int id) {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
    std::cout << "Thread " << id << " finished. Counter: " << counter << std::endl;
}

std::lock_guardを使って、カウンタのインクリメント操作がスレッドセーフになるようにしています。

メイン関数の定義

次に、メイン関数でスレッドを作成し、実行します。

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(incrementCounter, i);
    }

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

    std::cout << "Final Counter: " << counter << std::endl;
    return 0;
}

ここでは、10個のスレッドを作成し、それぞれがincrementCounter関数を実行します。すべてのスレッドが終了するまで待機し、最終的なカウンタの値を出力します。

コード全体の例

以上の内容をまとめると、以下のようになります。

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

std::mutex mtx;
int counter = 0;

void incrementCounter(int id) {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
    std::cout << "Thread " << id << " finished. Counter: " << counter << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(incrementCounter, i);
    }

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

    std::cout << "Final Counter: " << counter << std::endl;
    return 0;
}

この例では、10個のスレッドが並行してカウンタをインクリメントし、最終的なカウンタの値を出力します。std::lock_guardを使ってミューテックスを管理することで、データ競合を防ぎ、安全にカウンタを操作することができます。

次のセクションでは、複雑なデータ構造の共有について、さらに応用的な例を紹介します。

応用例:複雑なデータ構造の共有

ここでは、マルチスレッド環境で複雑なデータ構造を共有する応用例を紹介します。具体的には、複数のスレッドが並行してベクターにデータを追加し、そのデータを処理する例を示します。

準備:必要なヘッダとグローバル変数の定義

まず、必要なヘッダファイルをインクルードし、グローバル変数を定義します。

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

std::mutex mtx;
std::vector<int> data;

ここでは、std::mutexを使って排他制御を行い、共有データであるstd::vector<int>を定義しています。

データ追加関数の定義

次に、スレッドで実行される関数を定義します。この関数では、ベクターにデータを追加します。

void addData(int start, int end) {
    for (int i = start; i < end; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        data.push_back(i);
    }
}

std::lock_guardを使って、データ追加操作がスレッドセーフになるようにしています。

データ処理関数の定義

次に、ベクター内のデータを処理する関数を定義します。この関数では、ベクターのデータをソートし、出力します。

void processData() {
    std::lock_guard<std::mutex> lock(mtx);
    std::sort(data.begin(), data.end());
    for (const auto& val : data) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}

この関数では、ミューテックスを使ってベクター全体を保護し、データをソートして出力します。

メイン関数の定義

次に、メイン関数でスレッドを作成し、実行します。

int main() {
    std::vector<std::thread> threads;
    threads.emplace_back(addData, 0, 50);
    threads.emplace_back(addData, 50, 100);

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

    std::thread t(processData);
    t.join();

    return 0;
}

ここでは、データ追加用に2つのスレッドを作成し、範囲ごとにデータを追加します。すべてのデータ追加スレッドが終了した後で、データ処理スレッドを実行してデータをソートし、出力します。

コード全体の例

以上の内容をまとめると、以下のようになります。

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

std::mutex mtx;
std::vector<int> data;

void addData(int start, int end) {
    for (int i = start; i < end; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        data.push_back(i);
    }
}

void processData() {
    std::lock_guard<std::mutex> lock(mtx);
    std::sort(data.begin(), data.end());
    for (const auto& val : data) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    threads.emplace_back(addData, 0, 50);
    threads.emplace_back(addData, 50, 100);

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

    std::thread t(processData);
    t.join();

    return 0;
}

この例では、複数のスレッドが並行してデータを追加し、最後にデータをソートして出力します。std::lock_guardを使ってミューテックスを管理することで、データ競合を防ぎ、安全にデータを操作することができます。

次のセクションでは、理解を深めるための演習問題を提供します。

演習問題

以下の演習問題を通じて、マルチスレッド環境でのデータ共有に関する理解を深めましょう。各問題には、ヒントや解答例を提供しています。

問題1: 複数スレッドでのカウンタインクリメント

以下のコードを完成させて、10個のスレッドが並行してカウンタをインクリメントするプログラムを作成してください。カウンタの最終値が正しく表示されるようにミューテックスを使用してください。

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

std::mutex mtx;
int counter = 0;

void incrementCounter() {
    // TODO: ミューテックスを使用してカウンタをインクリメントする
}

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

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

    std::cout << "Final Counter: " << counter << std::endl;
    return 0;
}

ヒント

  • std::lock_guardを使ってミューテックスをロックし、カウンタのインクリメント操作をスレッドセーフにします。

解答例

void incrementCounter() {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
}

問題2: スレッド間での文字列共有

以下のコードを完成させて、5つのスレッドが並行して文字列をベクターに追加するプログラムを作成してください。文字列の追加操作がスレッドセーフになるようにミューテックスを使用してください。

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

std::mutex mtx;
std::vector<std::string> strings;

void addStrings(int id) {
    // TODO: ミューテックスを使用して文字列をベクターに追加する
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(addStrings, i);
    }

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

    for (const auto& str : strings) {
        std::cout << str << std::endl;
    }

    return 0;
}

ヒント

  • std::lock_guardを使ってミューテックスをロックし、文字列の追加操作をスレッドセーフにします。

解答例

void addStrings(int id) {
    for (int i = 0; i < 10; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        strings.push_back("Thread " + std::to_string(id) + " - String " + std::to_string(i));
    }
}

問題3: 条件変数を使ったスレッド同期

以下のコードを完成させて、メインスレッドがすべてのスレッドの処理完了を待機するプログラムを作成してください。条件変数を使用してスレッド間の同期を実現します。

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

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

void threadFunction(int id) {
    // TODO: 条件変数を使ってスレッド間の同期を実現する
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(threadFunction, i);
    }

    // メインスレッドを待機
    std::this_thread::sleep_for(std::chrono::seconds(1));

    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_all();

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

    return 0;
}

ヒント

  • スレッドが条件変数を待機し、readytrueになると処理を開始します。
  • std::unique_lockを使って条件変数を待機させます。

解答例

void threadFunction(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; });
    std::cout << "Thread " << id << " is processing" << std::endl;
}

これらの演習問題を通じて、マルチスレッド環境でのデータ共有に関するスキルを実践的に向上させることができます。次のセクションでは、この記事のまとめを行います。

まとめ

この記事では、C++のラムダ式とマルチスレッド環境でのデータ共有方法について詳しく解説しました。まず、ラムダ式の基本的な構文とキャプチャリストの使い方を学び、次にスレッドの基本とデータ競合の問題について理解しました。その後、ミューテックスを使ったデータ共有の解決方法、ラムダ式とスレッドの連携、そして条件変数を使ったスレッド間の同期について実践例を通して学びました。最後に、複雑なデータ構造の共有や応用例を紹介し、演習問題を通じて理解を深める機会を提供しました。

マルチスレッドプログラミングは、高度な並列処理を実現するための重要な技術です。適切にスレッドを管理し、データ共有の問題を解決することで、安全で効率的なプログラムを構築することができます。本記事を通じて得た知識を活用し、実際のプログラム開発に役立ててください。

コメント

コメントする

目次
  1. ラムダ式の基本
    1. ラムダ式の基本構文
    2. 基本例
    3. 省略可能な部分
    4. ラムダ式のキャプチャリスト
  2. ラムダ式のキャプチャリスト
    1. キャプチャリストの基本
    2. キャプチャの方法
  3. スレッドの基本
    1. スレッドの作成と実行
    2. スレッド関数の引数
    3. ラムダ式とスレッド
    4. スレッドの分離
    5. スレッドの終了とリソース管理
  4. スレッドとデータ共有の問題点
    1. データ競合(Race Condition)
    2. デッドロック(Deadlock)
    3. ライブロック(Livelock)
    4. データの一貫性の確保
  5. ミューテックスによるデータ共有の解決方法
    1. ミューテックスの基本的な使い方
    2. デッドロックの回避
    3. スピンロックの使用
    4. 再帰的ミューテックス
  6. ラムダ式とスレッドの連携
    1. ラムダ式を使ったスレッドの作成
    2. キャプチャリストによる変数の共有
    3. スレッド間のデータ同期
  7. 条件変数の使用
    1. 条件変数の基本的な使い方
    2. タイムアウトを使用した待機
    3. 生産者-消費者問題の実装
  8. 実践例:マルチスレッド環境でのデータ共有
    1. 準備:必要なヘッダとグローバル変数の定義
    2. スレッド関数の定義
    3. メイン関数の定義
    4. コード全体の例
  9. 応用例:複雑なデータ構造の共有
    1. 準備:必要なヘッダとグローバル変数の定義
    2. データ追加関数の定義
    3. データ処理関数の定義
    4. メイン関数の定義
    5. コード全体の例
  10. 演習問題
    1. 問題1: 複数スレッドでのカウンタインクリメント
    2. 問題2: スレッド間での文字列共有
    3. 問題3: 条件変数を使ったスレッド同期
  11. まとめ