C++20のstd::stop_tokenを使ったスレッドの安全な停止方法

C++20で導入された新機能であるstd::stop_tokenは、スレッドの安全かつ効率的な停止を実現するための新しい手法です。従来、スレッドの停止はフラグや条件変数を使用して行われてきましたが、これらの方法はコードの複雑化やバグの原因となることがありました。std::stop_tokenは、これらの問題を解決するために設計されており、シンプルで直感的なインターフェースを提供します。本記事では、std::stop_tokenの基本的な使い方から応用例までを詳しく解説し、安全なスレッド停止を実現するための実践的なガイドを提供します。

目次

std::stop_tokenとは?

std::stop_tokenは、C++20で導入されたスレッド制御のための新しい機能です。このトークンは、スレッドに停止要求を通知するための手段を提供します。従来、スレッドを停止するにはフラグを使用する方法が一般的でしたが、これにはスレッド間の競合や同期の問題が伴いました。

基本概念

std::stop_tokenは、スレッドが外部から停止を要求されたかどうかを簡単にチェックできる手段を提供します。これにより、スレッドの実行中に頻繁に停止要求を確認するコードを書く必要がなくなります。

stop_sourceとstop_callback

std::stop_tokenは、std::stop_sourceと連携して使用されます。std::stop_sourceはstop_tokenを生成し、停止要求を発行する役割を持ちます。また、std::stop_callbackを使用すると、停止要求が発行されたときに特定のコールバックを実行することができます。これにより、スレッドの停止処理をより柔軟に制御できます。

以下は、std::stop_tokenを使用した基本的な例です。

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

void worker(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Working...\n";
    }
    std::cout << "Stopped.\n";
}

int main() {
    std::jthread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop(); // スレッドに停止を要求
    return 0;
}

この例では、worker関数がstd::stop_tokenを受け取り、定期的にstop_requestedメソッドをチェックして停止要求があったかどうかを確認します。main関数では、1秒後にスレッドに停止要求を送信します。

std::jthreadとの連携

C++20で導入されたstd::jthread(joining thread)は、std::threadの改良版であり、スレッドの自動的な参加(join)を提供します。std::jthreadは、スレッドがスコープを離れる際に自動的に参加するため、リソースリークを防ぎます。また、std::jthreadは、std::stop_tokenとシームレスに連携することができ、スレッドの停止を容易に管理できます。

std::jthreadの基本的な使用方法

std::jthreadは、従来のstd::threadと同様にスレッドを作成できますが、停止要求を扱うためのstop_tokenが自動的に渡されます。以下はその基本的な例です。

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

void worker(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Working...\n";
    }
    std::cout << "Stopped.\n";
}

int main() {
    std::jthread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop(); // スレッドに停止を要求
    return 0;
}

この例では、std::jthreadworker関数を新しいスレッドで実行し、その際に自動的にstd::stop_tokenを渡します。main関数では、1秒後にスレッドに停止要求を送信します。

std::jthreadの利点

std::jthreadを使用する主な利点は以下の通りです。

  1. 自動的なスレッド参加:
    std::jthreadは、デストラクタが呼ばれる際に自動的にjoinするため、スレッドの未参加によるリソースリークを防ぎます。
  2. 簡単な停止要求の処理:
    std::jthreadは、std::stop_tokenを自動的に管理し、スレッド関数に渡すため、スレッドの停止要求を簡単に処理できます。
  3. コードの簡素化:
    std::jthreadの使用により、スレッドのライフサイクル管理が簡素化され、コードがより明確でエラーが少なくなります。

例: std::jthreadとstop_tokenの連携

以下の例は、std::jthreadとstd::stop_tokenの連携を示します。

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

void monitor(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Monitoring...\n";
    }
    std::cout << "Monitor stopped.\n";
}

int main() {
    std::jthread monitorThread(monitor);
    std::this_thread::sleep_for(std::chrono::seconds(2));
    monitorThread.request_stop(); // モニタースレッドに停止を要求
    return 0;
}

この例では、monitor関数が定期的にスレッドの停止要求をチェックし、停止要求があった場合にモニタリングを終了します。main関数では、2秒後にモニタースレッドに停止要求を送信します。

スレッドの停止フラグとの比較

std::stop_tokenは、従来のスレッド停止手法である停止フラグ(boolean flag)を用いた方法に比べて多くの利点を持っています。このセクションでは、従来の停止フラグとの違いと、std::stop_tokenの利点について詳しく比較します。

従来の停止フラグの方法

従来の方法では、スレッドの停止を制御するために、共有のbooleanフラグを使用します。以下はその基本的な例です。

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

std::atomic<bool> stopFlag{false};

void worker() {
    while (!stopFlag.load()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Working...\n";
    }
    std::cout << "Stopped.\n";
}

int main() {
    std::thread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    stopFlag.store(true); // スレッドに停止を要求
    t.join();
    return 0;
}

この例では、stopFlagを使用してスレッドの停止を制御しますが、この方法には以下の問題点があります。

従来の方法の問題点

  1. 同期の難しさ:
    スレッド間で共有されるフラグは、適切な同期が必要です。特に、複数のスレッドが同じフラグをチェックする場合、データ競合やレースコンディションのリスクがあります。
  2. コールバックの欠如:
    フラグを用いた方法では、停止要求が発行された際に即座にコールバックを実行することができません。これにより、リソースのクリーンアップや特定の停止処理を行うのが難しくなります。
  3. コードの複雑化:
    停止フラグを用いる方法は、フラグのチェックや更新のための追加のコードが必要となり、コードが複雑になります。

std::stop_tokenの利点

std::stop_tokenは、これらの問題を解決するために設計されています。

  1. 簡単な同期:
    std::stop_tokenとstd::stop_sourceは、内部で適切に同期されているため、ユーザーは同期の問題を気にする必要がありません。
  2. コールバック機能:
    std::stop_callbackを使用することで、停止要求が発行された際に即座にコールバックを実行できます。これにより、リソースのクリーンアップや特定の処理を簡単に行うことができます。
  3. コードの簡素化:
    std::stop_tokenを使用することで、停止要求の管理が簡素化され、コードがより直感的で明確になります。

例: std::stop_tokenによるスレッド停止

以下の例では、std::stop_tokenを使用してスレッドを停止する方法を示します。

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

void worker(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Working...\n";
    }
    std::cout << "Stopped.\n";
}

int main() {
    std::jthread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop(); // スレッドに停止を要求
    return 0;
}

この例では、std::stop_tokenを使用することで、停止要求の管理が簡素化され、スレッドの停止がより直感的に行えます。std::stop_tokenは、同期の問題を解決し、コールバック機能を提供することで、従来の停止フラグに比べて多くの利点をもたらします。

実装例: 基本的な使い方

ここでは、std::stop_tokenを使ったスレッド停止の基本的な実装例を紹介します。std::stop_tokenを用いることで、スレッドの停止を簡単かつ安全に管理できます。

基本的なコード例

以下のコード例は、std::stop_tokenを使用してスレッドの停止要求を処理する方法を示しています。

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

void worker(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Working...\n";
    }
    std::cout << "Stopped.\n";
}

int main() {
    std::jthread t(worker); // std::jthreadは自動的にstop_tokenを渡す
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop(); // スレッドに停止を要求
    return 0;
}

この例では、worker関数がstd::stop_tokenを引数として受け取り、定期的にstop_requestedメソッドをチェックして停止要求があったかどうかを確認します。main関数では、1秒後にスレッドに停止要求を送信します。

コードの詳細解説

worker関数

worker関数は、std::stop_tokenを受け取ります。このトークンを使用して、スレッドが停止要求を受け取ったかどうかをチェックします。

void worker(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Working...\n";
    }
    std::cout << "Stopped.\n";
}

この関数内では、停止要求がない限り、定期的に「Working…」と表示します。停止要求が発行されると、ループを抜けて「Stopped.」と表示します。

main関数

main関数では、std::jthreadを使用してworkerスレッドを起動し、1秒後に停止要求を発行します。

int main() {
    std::jthread t(worker); // std::jthreadは自動的にstop_tokenを渡す
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop(); // スレッドに停止を要求
    return 0;
}

std::jthreadは、スレッドがスコープを離れる際に自動的に参加(join)するため、手動でjoinを呼び出す必要はありません。また、std::jthreadは自動的にstd::stop_tokenを管理し、スレッドに渡します。

応用例: 複数のスレッドでの利用

以下のコード例では、複数のスレッドを起動し、std::stop_tokenを使用してすべてのスレッドに停止要求を送信します。

#include <iostream>
#include <thread>
#include <stop_token>
#include <vector>
#include <chrono>

void worker(std::stop_token stopToken, int id) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Worker " << id << " working...\n";
    }
    std::cout << "Worker " << id << " stopped.\n";
}

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

    std::this_thread::sleep_for(std::chrono::seconds(1));
    for (auto& t : threads) {
        t.request_stop(); // すべてのスレッドに停止を要求
    }

    return 0;
}

この例では、5つのworkerスレッドを起動し、1秒後にすべてのスレッドに停止要求を送信します。各スレッドは、停止要求を受け取ると「stopped」と表示して終了します。

これにより、std::stop_tokenを使った基本的なスレッド停止方法が理解できたと思います。次は応用例や複数スレッドの管理方法を詳しく見ていきましょう。

応用例: 複数スレッドの管理

std::stop_tokenを使用することで、複数のスレッドを効率的に管理し、必要に応じて停止要求を発行することができます。このセクションでは、複数のスレッドを管理する際のstd::stop_tokenの応用例を紹介します。

マルチスレッド環境でのタスク管理

以下の例では、タスク管理のために複数のスレッドを起動し、std::stop_tokenを使用して停止要求を発行します。これにより、各スレッドが安全に停止することができます。

#include <iostream>
#include <thread>
#include <stop_token>
#include <vector>
#include <chrono>

void task(std::stop_token stopToken, int id) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Task " << id << " is running...\n";
    }
    std::cout << "Task " << id << " has stopped.\n";
}

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

    std::this_thread::sleep_for(std::chrono::seconds(2));
    for (auto& t : threads) {
        t.request_stop(); // すべてのタスクに停止を要求
    }

    return 0;
}

この例では、5つのタスクスレッドを起動し、各タスクは定期的に「Task X is running…」と表示します。2秒後にすべてのスレッドに停止要求を送信し、各タスクは「Task X has stopped.」と表示して終了します。

複数スレッドの一括停止

std::stop_sourceを使用して、複数のstd::stop_tokenに対して一括で停止要求を発行することも可能です。以下の例では、std::stop_sourceを使って複数のスレッドに停止要求を送信します。

#include <iostream>
#include <thread>
#include <stop_token>
#include <vector>
#include <chrono>

void task(std::stop_token stopToken, int id) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Task " << id << " is running...\n";
    }
    std::cout << "Task " << id << " has stopped.\n";
}

int main() {
    std::stop_source stopSource;
    std::vector<std::jthread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(task, stopSource.get_token(), i);
    }

    std::this_thread::sleep_for(std::chrono::seconds(2));
    stopSource.request_stop(); // すべてのタスクに一括で停止を要求

    return 0;
}

この例では、std::stop_sourceを使用して、すべてのスレッドに一括で停止要求を送信します。各スレッドは停止要求を受け取ると、「Task X has stopped.」と表示して終了します。

スレッド間での通知機能

std::stop_tokenは、スレッド間での通知機能としても使用できます。以下の例では、スレッドAがスレッドBに停止要求を送信するシナリオを示します。

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

void workerB(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Worker B is running...\n";
    }
    std::cout << "Worker B has stopped.\n";
}

void workerA(std::stop_source& stopSource) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    stopSource.request_stop(); // Worker Bに停止を要求
}

int main() {
    std::stop_source stopSource;
    std::jthread b(workerB, stopSource.get_token());
    std::jthread a(workerA, std::ref(stopSource));

    return 0;
}

この例では、workerAが1秒後にstopSourceを使用してworkerBに停止要求を送信します。workerBは、停止要求を受け取ると「Worker B has stopped.」と表示して終了します。

これらの例から、std::stop_tokenとstd::stop_sourceを活用することで、複数のスレッドを効率的に管理し、柔軟に停止要求を発行することが可能であることがわかります。次は、エラーハンドリングと例外処理について詳しく解説します。

エラーハンドリングと例外処理

スレッドの停止要求が発行される際には、適切なエラーハンドリングと例外処理を行うことが重要です。これにより、スレッドが安全かつ確実に停止し、リソースが適切にクリーンアップされるようにします。このセクションでは、std::stop_tokenを使用したスレッドの停止中に発生する可能性のあるエラーや例外の処理方法について解説します。

エラーハンドリングの基本

スレッド内でエラーが発生した場合、そのエラーを適切にキャッチして処理することが必要です。以下のコード例では、スレッド内での例外処理を示します。

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

void worker(std::stop_token stopToken) {
    try {
        while (!stopToken.stop_requested()) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            if (/* エラー条件 */) {
                throw std::runtime_error("エラーが発生しました");
            }
            std::cout << "Working...\n";
        }
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << "\n";
    }
    std::cout << "Stopped.\n";
}

int main() {
    std::jthread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop();
    return 0;
}

この例では、worker関数内で例外が発生した場合にキャッチし、エラーメッセージを表示します。例外がキャッチされると、スレッドは停止処理に移行します。

例外処理の詳細

スレッド内で発生する例外を適切に処理することで、スレッドの異常終了を防ぎ、リソースのリークを回避できます。以下のコード例では、例外が発生した際にリソースを適切にクリーンアップする方法を示します。

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

void worker(std::stop_token stopToken) {
    try {
        // リソースの初期化
        int* resource = new int[100];

        while (!stopToken.stop_requested()) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            if (/* エラー条件 */) {
                throw std::runtime_error("エラーが発生しました");
            }
            std::cout << "Working...\n";
        }

        // リソースのクリーンアップ
        delete[] resource;
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << "\n";
        // リソースのクリーンアップ
        delete[] resource;
    }
    std::cout << "Stopped.\n";
}

int main() {
    std::jthread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop();
    return 0;
}

この例では、worker関数内で動的に割り当てたリソースを、例外が発生した場合にも確実にクリーンアップしています。これにより、リソースリークを防ぎます。

コールバックを使用した停止処理

std::stop_tokenとstd::stop_callbackを組み合わせて使用することで、停止要求が発行された際に特定の処理を行うことができます。以下のコード例では、停止要求が発行された際にクリーンアップ処理を行います。

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

void worker(std::stop_token stopToken) {
    int* resource = new int[100];
    std::stop_callback callback(stopToken, [&resource]() {
        // 停止要求が発行された際のクリーンアップ処理
        delete[] resource;
        std::cout << "Resource cleaned up.\n";
    });

    try {
        while (!stopToken.stop_requested()) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            if (/* エラー条件 */) {
                throw std::runtime_error("エラーが発生しました");
            }
            std::cout << "Working...\n";
        }
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << "\n";
    }
    std::cout << "Stopped.\n";
}

int main() {
    std::jthread t(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop();
    return 0;
}

この例では、std::stop_callbackを使用して、停止要求が発行された際にリソースのクリーンアップ処理を行っています。これにより、例外が発生した場合でも確実にリソースが解放されます。

以上のように、std::stop_tokenを使用することで、スレッドの停止中に発生するエラーや例外を適切に処理し、リソースを安全に管理することができます。次に、std::stop_tokenの使用がスレッドのパフォーマンスに与える影響について検証します。

パフォーマンスへの影響

std::stop_tokenの使用がスレッドのパフォーマンスに与える影響について検証します。スレッドの停止要求を管理するための仕組みは、システムのパフォーマンスに影響を与える可能性があります。ここでは、std::stop_tokenのパフォーマンスへの影響を評価し、従来の方法と比較します。

パフォーマンスの評価

std::stop_tokenを使用することで、スレッドの停止要求が効率的に処理されることが期待されますが、これが実際のパフォーマンスにどのように影響するかを検証するために、以下のようなテストを行います。

  • スレッドの起動および停止にかかる時間
  • 停止要求のチェック頻度によるオーバーヘッド
  • 従来の停止フラグとの比較

テストコード例

以下のテストコードでは、std::stop_tokenと従来の停止フラグを使用して、スレッドの停止要求の処理時間を測定します。

#include <iostream>
#include <thread>
#include <stop_token>
#include <atomic>
#include <chrono>

void stopTokenWorker(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        // simulate work
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

void flagWorker(std::atomic<bool>& stopFlag) {
    while (!stopFlag.load()) {
        // simulate work
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    // Test std::stop_token
    auto start = std::chrono::high_resolution_clock::now();
    std::jthread stopTokenThread(stopTokenWorker);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    stopTokenThread.request_stop();
    auto stop = std::chrono::high_resolution_clock::now();
    auto stopTokenDuration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start).count();

    // Test flag
    std::atomic<bool> stopFlag{false};
    start = std::chrono::high_resolution_clock::now();
    std::thread flagThread(flagWorker, std::ref(stopFlag));
    std::this_thread::sleep_for(std::chrono::seconds(1));
    stopFlag.store(true);
    flagThread.join();
    stop = std::chrono::high_resolution_clock::now();
    auto flagDuration = std::chrono::duration_cast<std::chrono::microseconds>(stop - start).count();

    // Output results
    std::cout << "std::stop_token duration: " << stopTokenDuration << " microseconds\n";
    std::cout << "Flag duration: " << flagDuration << " microseconds\n";

    return 0;
}

パフォーマンス結果の解析

このテストコードでは、std::stop_tokenと従来の停止フラグを使用して、それぞれのスレッドの停止にかかる時間を測定します。実行結果を比較することで、std::stop_tokenのパフォーマンス特性を評価できます。

期待される結果と考察

std::stop_tokenを使用した場合、停止要求の処理が効率的に行われるため、従来の停止フラグと比較して同等かそれ以上のパフォーマンスが期待されます。以下に、期待される結果の例を示します。

std::stop_token duration: 1000500 microseconds
Flag duration: 1001000 microseconds

この結果から、std::stop_tokenは停止フラグと比較してほぼ同等のパフォーマンスを持ちつつ、より直感的で安全なスレッド停止メカニズムを提供していることがわかります。

パフォーマンスの改善ポイント

std::stop_tokenのパフォーマンスをさらに向上させるために、以下のポイントを考慮することができます。

  • 停止要求のチェック頻度を適切に設定し、必要以上に頻繁にチェックしないようにする
  • 不要な停止要求の発行を避け、実際に必要なときにのみ停止要求を発行する
  • スレッド内での処理負荷を適切に管理し、必要に応じてスレッドプールなどを活用する

これらのポイントを考慮することで、std::stop_tokenを使用したスレッド停止のパフォーマンスを最適化できます。次に、実際のプロジェクトでの活用事例について見ていきます。

実際のプロジェクトでの活用事例

実際のプロジェクトでstd::stop_tokenをどのように活用できるかを具体的な事例を通して紹介します。std::stop_tokenは、複雑なスレッド管理をシンプルにし、停止要求を効果的に処理するための強力なツールです。

事例1: サーバーのリクエスト処理

あるサーバーアプリケーションでは、クライアントからのリクエストを処理するために複数のスレッドを使用しています。メンテナンスやシャットダウンの際には、これらのスレッドを安全に停止させる必要があります。

#include <iostream>
#include <thread>
#include <stop_token>
#include <vector>
#include <chrono>

void handleRequest(std::stop_token stopToken, int clientId) {
    while (!stopToken.stop_requested()) {
        // リクエスト処理シミュレーション
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Handling request from client " << clientId << "\n";
    }
    std::cout << "Stopped handling requests for client " << clientId << "\n";
}

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

    // サーバーは一定時間動作する
    std::this_thread::sleep_for(std::chrono::seconds(5));

    // メンテナンスのためすべてのスレッドに停止要求を送信
    for (auto& t : threads) {
        t.request_stop();
    }

    return 0;
}

この例では、サーバーが複数のクライアントリクエストを処理しており、メンテナンスのためにすべてのスレッドに停止要求を送信しています。各スレッドは、停止要求を受け取るとリクエスト処理を終了し、安全に停止します。

事例2: データ処理パイプライン

データ処理パイプラインでは、複数のステージでデータが処理されます。各ステージはスレッドとして実行され、データの流れを管理します。停止要求を受け取った際に、すべてのスレッドを安全に停止させる必要があります。

#include <iostream>
#include <thread>
#include <stop_token>
#include <vector>
#include <chrono>

void processData(std::stop_token stopToken, int stageId) {
    while (!stopToken.stop_requested()) {
        // データ処理シミュレーション
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Processing data at stage " << stageId << "\n";
    }
    std::cout << "Stopped processing data at stage " << stageId << "\n";
}

int main() {
    std::vector<std::jthread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(processData, i);
    }

    // パイプラインは一定時間動作する
    std::this_thread::sleep_for(std::chrono::seconds(5));

    // 停止要求をすべてのスレッドに送信
    for (auto& t : threads) {
        t.request_stop();
    }

    return 0;
}

この例では、データ処理の各ステージがスレッドとして実行され、一定時間後にすべてのスレッドに停止要求を送信します。各スレッドは、停止要求を受け取るとデータ処理を終了し、安全に停止します。

事例3: リアルタイムデータ収集システム

リアルタイムデータ収集システムでは、センサーからのデータを継続的に収集し、解析のために別のプロセスに渡します。システムのシャットダウンやメンテナンスの際には、すべてのデータ収集スレッドを安全に停止させる必要があります。

#include <iostream>
#include <thread>
#include <stop_token>
#include <vector>
#include <chrono>

void collectData(std::stop_token stopToken, int sensorId) {
    while (!stopToken.stop_requested()) {
        // データ収集シミュレーション
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Collecting data from sensor " << sensorId << "\n";
    }
    std::cout << "Stopped collecting data from sensor " << sensorId << "\n";
}

int main() {
    std::vector<std::jthread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(collectData, i);
    }

    // データ収集は一定時間動作する
    std::this_thread::sleep_for(std::chrono::seconds(5));

    // 停止要求をすべてのスレッドに送信
    for (auto& t : threads) {
        t.request_stop();
    }

    return 0;
}

この例では、センサーからのデータ収集を行うスレッドが複数存在し、一定時間後にすべてのスレッドに停止要求を送信します。各スレッドは、停止要求を受け取るとデータ収集を終了し、安全に停止します。

これらの事例から、std::stop_tokenは複数のスレッドを管理し、必要に応じて停止要求を安全かつ効率的に発行するための強力なツールであることがわかります。次に、読者が学んだ内容を実践するための演習問題を提供します。

演習問題

ここでは、std::stop_tokenを使用したスレッド停止の理解を深めるための演習問題を提供します。これらの問題を通じて、std::stop_tokenの実践的な使い方を学び、スレッド管理のスキルを向上させましょう。

問題1: 基本的なスレッド停止

std::stop_tokenを使用して、以下の要件を満たすプログラムを作成してください。

  1. 3つのスレッドを起動し、それぞれが異なるメッセージを定期的に表示します。
  2. メインスレッドで5秒間待機した後、すべてのスレッドに停止要求を送信します。
  3. 各スレッドは、停止要求を受け取った後に停止メッセージを表示して終了します。

問題2: 停止要求のコールバック

std::stop_tokenとstd::stop_callbackを使用して、以下の要件を満たすプログラムを作成してください。

  1. 2つのスレッドを起動し、それぞれが異なるセンサーからデータを収集するシミュレーションを行います。
  2. メインスレッドで3秒間待機した後、すべてのスレッドに停止要求を送信します。
  3. 各スレッドは、停止要求を受け取った際にリソースのクリーンアップ処理を行い、停止メッセージを表示して終了します。

問題3: パフォーマンスの比較

以下の要件を満たすプログラムを作成し、std::stop_tokenと従来の停止フラグのパフォーマンスを比較してください。

  1. std::stop_tokenを使用したスレッド停止の実装を作成します。
  2. 従来の停止フラグを使用したスレッド停止の実装を作成します。
  3. それぞれの実装でスレッドを10個起動し、各スレッドが1秒ごとに停止要求をチェックする処理を行います。
  4. メインスレッドで10秒間待機した後、すべてのスレッドに停止要求を送信します。
  5. 各実装の停止処理にかかる時間を計測し、結果を比較してください。

問題4: スレッド間の通知機能

std::stop_tokenを使用して、スレッド間で通知機能を実装してください。

  1. 2つのスレッドを起動し、1つはデータ処理を行い、もう1つは監視を行います。
  2. 監視スレッドが定期的にデータ処理スレッドの状況をチェックし、一定条件が満たされた場合に停止要求を送信します。
  3. データ処理スレッドは、停止要求を受け取った際にクリーンアップ処理を行い、停止メッセージを表示して終了します。

問題5: マルチスレッドタスク管理

以下の要件を満たすプログラムを作成してください。

  1. 5つのスレッドを起動し、それぞれが異なるタスクを実行します。
  2. メインスレッドで一定の条件が満たされた場合、特定のスレッドに対してのみ停止要求を送信します。
  3. 停止要求を受け取ったスレッドは、クリーンアップ処理を行い、停止メッセージを表示して終了します。

これらの演習問題を通じて、std::stop_tokenの使い方やスレッド管理の方法を実践的に学ぶことができます。次に、本記事の内容を総括し、std::stop_tokenの利便性と重要性を再確認します。

まとめ

本記事では、C++20で導入されたstd::stop_tokenを使用してスレッドを安全かつ効率的に停止する方法について詳しく解説しました。std::stop_tokenは、従来の停止フラグや条件変数を用いた方法に比べて、コードの簡素化や同期の問題の軽減、コールバックによる柔軟な停止処理など多くの利点を提供します。

主要ポイントのまとめ

  1. std::stop_tokenとは?:
    std::stop_tokenは、スレッドに対して停止要求を通知するための手段を提供します。stop_sourceと連携して使用され、スレッドの停止を簡単に制御できます。
  2. std::jthreadとの連携:
    std::jthreadは、std::stop_tokenを自動的に管理し、スレッドのスコープを離れる際に自動的に参加(join)するため、リソースリークを防ぎます。
  3. スレッドの停止フラグとの比較:
    std::stop_tokenは、従来の停止フラグに比べて同期の問題が少なく、コールバック機能を提供することでコードの複雑さを軽減します。
  4. 基本的な使い方:
    std::stop_tokenを使用してスレッドを停止する基本的な方法を学び、実装例を通じて具体的な利用方法を確認しました。
  5. 応用例:
    複数スレッドの管理や停止要求の一括送信、スレッド間の通知機能など、std::stop_tokenの応用例を紹介しました。
  6. エラーハンドリングと例外処理:
    スレッドの停止中に発生する可能性のあるエラーや例外を適切に処理する方法について解説し、リソースの安全な管理方法を学びました。
  7. パフォーマンスへの影響:
    std::stop_tokenを使用した場合のパフォーマンスを評価し、従来の方法と比較して同等かそれ以上のパフォーマンスが期待できることを確認しました。
  8. 実際のプロジェクトでの活用事例:
    サーバーのリクエスト処理、データ処理パイプライン、リアルタイムデータ収集システムなど、実際のプロジェクトでのstd::stop_tokenの活用事例を紹介しました。
  9. 演習問題:
    std::stop_tokenの実践的な使い方を学ぶための演習問題を提供し、理解を深めるための具体的な課題を提示しました。

std::stop_tokenを活用することで、スレッドの停止処理がより直感的で安全になります。これにより、マルチスレッドプログラミングが容易になり、効率的でバグの少ないコードを実装できるようになります。今後のプロジェクトで、std::stop_tokenを活用し、スレッド管理のスキルを向上させましょう。

コメント

コメントする

目次