C++20のstd::stop_tokenで非同期タスクのキャンセルを実現する方法

C++20では、非同期プログラミングが大幅に改善され、非同期タスクのキャンセルが容易になりました。その中でも、std::stop_tokenは特に注目すべき機能です。本記事では、std::stop_tokenの基本的な使い方から、実際のコード例を交えた応用までを詳しく解説します。これにより、C++を使用している開発者が非同期タスクのキャンセルを効率的に実装できるようになります。例えば、長時間実行されるタスクやユーザーが中断を要求する可能性のある処理において、この機能は非常に有用です。非同期タスクのキャンセル処理は、ソフトウェアのレスポンス向上とユーザー体験の向上に寄与するため、ぜひマスターしていただきたい技術です。

目次

std::stop_tokenの概要

C++20で導入されたstd::stop_tokenは、非同期タスクをキャンセルするための新しいメカニズムです。従来、非同期タスクのキャンセルは、フラグを使用したり、独自のロジックを実装する必要がありましたが、std::stop_tokenを使用することで、より簡潔で直感的なキャンセル処理が可能となります。

std::stop_tokenとは

std::stop_tokenは、タスクの停止を要求するためのトークンです。このトークンをタスクに渡し、キャンセルが要求されたかどうかをチェックすることで、タスクを中断することができます。トークンは、キャンセルを要求する側(std::stop_source)と、キャンセルを受け取る側(std::stop_token)で構成されます。

基本的な使い方

以下に、std::stop_tokenの基本的な使い方を示します。

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

void task(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Running task..." << std::endl;
    }
    std::cout << "Task was stopped." << std::endl;
}

int main() {
    std::jthread worker(task);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    worker.request_stop(); // タスクの停止を要求
    worker.join();
    return 0;
}

このコードでは、std::jthreadを使用して非同期タスクを実行し、1秒後にタスクの停止を要求しています。タスクは、stop_tokenを使用して停止要求を確認し、停止要求があればループを抜けて終了します。

非同期タスクの実装例

非同期タスクを実装する際、std::stop_tokenを利用してキャンセル処理を組み込むことで、タスクの柔軟性と制御性を向上させることができます。ここでは、具体的なコード例を通じて、非同期タスクの実装方法を説明します。

基本的な非同期タスクの実装

まず、std::stop_tokenを使用しない基本的な非同期タスクの実装例を見てみましょう。

#include <iostream>
#include <thread>

void simpleTask() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        std::cout << "Task running: " << i << std::endl;
    }
    std::cout << "Task completed." << std::endl;
}

int main() {
    std::thread worker(simpleTask);
    worker.join();
    return 0;
}

このコードでは、simpleTask関数を別スレッドで実行し、10回の反復処理を行っています。しかし、このタスクにはキャンセル機能がありません。

std::stop_tokenを使った非同期タスクの実装

次に、std::stop_tokenを使用して非同期タスクにキャンセル機能を追加した実装例を紹介します。

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

void cancellableTask(std::stop_token stopToken) {
    for (int i = 0; i < 10; ++i) {
        if (stopToken.stop_requested()) {
            std::cout << "Task was cancelled." << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        std::cout << "Task running: " << i << std::endl;
    }
    std::cout << "Task completed." << std::endl;
}

int main() {
    std::jthread worker(cancellableTask);
    std::this_thread::sleep_for(std::chrono::seconds(2));
    worker.request_stop(); // タスクの停止を要求
    worker.join();
    return 0;
}

このコードでは、cancellableTask関数にstd::stop_tokenを渡し、タスクの各反復ごとにstop_requestedメソッドを使用してキャンセル要求を確認しています。main関数では、2秒後にタスクの停止を要求し、workerスレッドが停止要求を受け取ると、タスクが中断されます。

このように、std::stop_tokenを使用することで、非同期タスクの実装がより柔軟で制御しやすくなります。次の項目では、さらに詳細なキャンセル処理の方法について解説します。

std::stop_tokenを使ったキャンセル処理

std::stop_tokenを使用することで、非同期タスクのキャンセル処理が簡単に実装できます。ここでは、具体的な手順を通じて、std::stop_tokenを使ったキャンセル処理の詳細を説明します。

キャンセル処理の基本手順

非同期タスクをキャンセルするための基本手順は以下の通りです。

  1. std::stop_sourceオブジェクトを作成する。
  2. std::stop_sourceからstd::stop_tokenを取得する。
  3. std::stop_tokenをタスクに渡す。
  4. タスク内でstop_requestedメソッドをチェックする。
  5. タスクの停止を要求する場合は、std::stop_sourceのrequest_stopメソッドを呼び出す。

以下に具体的なコード例を示します。

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

void cancellableTask(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Task is running..." << std::endl;
    }
    std::cout << "Task was cancelled." << std::endl;
}

int main() {
    std::stop_source stopSource;
    std::jthread worker(cancellableTask, stopSource.get_token());

    std::this_thread::sleep_for(std::chrono::seconds(1));
    stopSource.request_stop(); // タスクの停止を要求
    worker.join();
    return 0;
}

このコードでは、std::stop_sourceオブジェクトを作成し、そのget_tokenメソッドでstd::stop_tokenを取得しています。取得したstop_tokenをタスク(cancellableTask)に渡し、タスク内でstop_requestedメソッドを使用して停止要求を確認しています。main関数内では、1秒後にstopSourceのrequest_stopメソッドを呼び出してタスクの停止を要求しています。

std::stop_tokenとコールバック関数

std::stop_tokenには、停止が要求されたときに実行されるコールバック関数を登録することもできます。以下にその例を示します。

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

void cancellableTaskWithCallback(std::stop_token stopToken) {
    std::stop_callback callback(stopToken, []() {
        std::cout << "Stop requested, running cleanup tasks..." << std::endl;
    });

    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Task is running..." << std::endl;
    }
    std::cout << "Task was cancelled." << std::endl;
}

int main() {
    std::stop_source stopSource;
    std::jthread worker(cancellableTaskWithCallback, stopSource.get_token());

    std::this_thread::sleep_for(std::chrono::seconds(1));
    stopSource.request_stop(); // タスクの停止を要求
    worker.join();
    return 0;
}

この例では、std::stop_callbackを使用して、停止が要求されたときに実行されるコールバック関数を登録しています。タスクがキャンセルされると、コールバック関数が実行され、後続のクリーンアップ処理が行われます。

これらの手法を組み合わせることで、より柔軟で効率的なキャンセル処理を実現できます。次の項目では、std::stop_sourceとの連携方法について解説します。

std::stop_sourceとの連携

std::stop_tokenを用いた非同期タスクのキャンセル処理において、std::stop_sourceとの連携は重要です。std::stop_sourceは、キャンセルリクエストの発行者であり、std::stop_tokenとともに機能します。ここでは、std::stop_sourceとstd::stop_tokenの連携方法とその利点について詳しく解説します。

std::stop_sourceとは

std::stop_sourceは、停止リクエストを発行するためのオブジェクトです。これにより、関連するstd::stop_tokenに対して停止要求を伝えることができます。std::stop_sourceオブジェクトは、std::stop_tokenを生成し、そのトークンをタスクに渡します。

std::stop_sourceの基本操作

以下の例では、std::stop_sourceを使用して非同期タスクをキャンセルする方法を示します。

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

void taskWithStopSource(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Task is running..." << std::endl;
    }
    std::cout << "Task was cancelled." << std::endl;
}

int main() {
    std::stop_source stopSource;
    std::jthread worker(taskWithStopSource, stopSource.get_token());

    std::this_thread::sleep_for(std::chrono::seconds(1));
    stopSource.request_stop(); // タスクの停止を要求
    worker.join();
    return 0;
}

このコードでは、std::stop_sourceオブジェクトを作成し、そのget_tokenメソッドを用いてstd::stop_tokenを取得しています。取得したstop_tokenをタスクに渡し、タスクはstop_requestedメソッドを使用して停止要求を確認しています。main関数では、stopSourceのrequest_stopメソッドを呼び出してタスクの停止を要求しています。

複数タスクでのstd::stop_sourceの利用

std::stop_sourceは、複数のタスクに対して同時にキャンセルリクエストを発行することができます。これにより、複数の関連タスクを一括してキャンセルする場合に便利です。

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

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..." << std::endl;
    }
    std::cout << "Task " << id << " was cancelled." << std::endl;
}

int main() {
    std::stop_source stopSource;
    std::jthread worker1(task, stopSource.get_token(), 1);
    std::jthread worker2(task, stopSource.get_token(), 2);

    std::this_thread::sleep_for(std::chrono::seconds(1));
    stopSource.request_stop(); // すべてのタスクの停止を要求
    worker1.join();
    worker2.join();
    return 0;
}

この例では、std::stop_sourceから取得したstop_tokenを複数のタスクに渡しています。stopSourceのrequest_stopメソッドを呼び出すことで、すべてのタスクに対して一括して停止要求を発行できます。

std::stop_sourceの利点

std::stop_sourceを利用することで、以下の利点があります。

  1. 統一的なキャンセル管理:複数のタスクに対して一元的にキャンセルリクエストを管理できるため、コードがシンプルで分かりやすくなります。
  2. 柔軟性の向上:必要に応じて、任意のタイミングでキャンセルリクエストを発行できるため、動的なタスク管理が可能です。
  3. コードの再利用性:キャンセル処理を標準化することで、コードの再利用性が向上し、メンテナンスが容易になります。

次の項目では、キャンセル可能な非同期タスクの設計について解説します。

キャンセル可能な非同期タスクの設計

非同期タスクを設計する際に、キャンセル可能な構造を持たせることで、アプリケーションの柔軟性と応答性を向上させることができます。ここでは、キャンセル可能な非同期タスクを設計する際のベストプラクティスを紹介します。

キャンセルポイントの設定

キャンセルポイントとは、タスクの実行中にキャンセル要求をチェックし、中断するためのポイントです。長時間実行されるタスクの場合、適切な間隔でキャンセルポイントを設けることが重要です。

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

void taskWithCancellationPoints(std::stop_token stopToken) {
    for (int i = 0; i < 100; ++i) {
        if (stopToken.stop_requested()) {
            std::cout << "Task was cancelled at iteration " << i << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        std::cout << "Task running: iteration " << i << std::endl;
    }
    std::cout << "Task completed successfully." << std::endl;
}

int main() {
    std::stop_source stopSource;
    std::jthread worker(taskWithCancellationPoints, stopSource.get_token());

    std::this_thread::sleep_for(std::chrono::seconds(2));
    stopSource.request_stop(); // タスクの停止を要求
    worker.join();
    return 0;
}

このコードでは、100回の反復処理の各ステップでstop_requestedメソッドを使用してキャンセル要求をチェックしています。これにより、適切なタイミングでタスクを中断できます。

リソースの安全な解放

キャンセルされたタスクが確実にリソースを解放するように設計することが重要です。例として、ファイルやネットワークリソースの解放処理を適切に行う方法を示します。

#include <iostream>
#include <thread>
#include <stop_token>
#include <fstream>

void fileProcessingTask(std::stop_token stopToken) {
    std::ifstream file("largefile.txt");
    if (!file.is_open()) {
        std::cerr << "Failed to open file." << std::endl;
        return;
    }

    std::string line;
    while (std::getline(file, line)) {
        if (stopToken.stop_requested()) {
            std::cout << "Task was cancelled during file processing." << std::endl;
            file.close();
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate processing time
        std::cout << "Processing line: " << line << std::endl;
    }
    file.close();
    std::cout << "File processing completed successfully." << std::endl;
}

int main() {
    std::stop_source stopSource;
    std::jthread worker(fileProcessingTask, stopSource.get_token());

    std::this_thread::sleep_for(std::chrono::seconds(2));
    stopSource.request_stop(); // タスクの停止を要求
    worker.join();
    return 0;
}

この例では、ファイル処理タスクがキャンセル要求をチェックし、キャンセルされた場合にファイルを閉じてリソースを適切に解放します。

タスクの状態管理

キャンセル可能なタスクは、状態管理も重要です。タスクの進行状況や現在の状態を管理し、適切に対応できるように設計します。

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

enum class TaskState {
    NotStarted,
    Running,
    Cancelled,
    Completed
};

void statefulTask(std::stop_token stopToken, std::atomic<TaskState>& state) {
    state.store(TaskState::Running);
    for (int i = 0; i < 100; ++i) {
        if (stopToken.stop_requested()) {
            state.store(TaskState::Cancelled);
            std::cout << "Task was cancelled at iteration " << i << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        std::cout << "Task running: iteration " << i << std::endl;
    }
    state.store(TaskState::Completed);
    std::cout << "Task completed successfully." << std::endl;
}

int main() {
    std::stop_source stopSource;
    std::atomic<TaskState> state(TaskState::NotStarted);
    std::jthread worker(statefulTask, stopSource.get_token(), std::ref(state));

    std::this_thread::sleep_for(std::chrono::seconds(2));
    stopSource.request_stop(); // タスクの停止を要求
    worker.join();

    TaskState finalState = state.load();
    switch (finalState) {
        case TaskState::Cancelled:
            std::cout << "Task was cancelled." << std::endl;
            break;
        case TaskState::Completed:
            std::cout << "Task completed." << std::endl;
            break;
        default:
            std::cout << "Task did not run." << std::endl;
            break;
    }

    return 0;
}

この例では、タスクの状態をstd::atomicで管理し、タスクの進行状況や最終状態を把握しています。

これらの設計パターンを取り入れることで、キャンセル可能な非同期タスクを効率的かつ安全に実装できます。次の項目では、std::jthreadの活用方法について解説します。

std::jthreadの活用

C++20で導入されたstd::jthreadは、キャンセル可能なスレッドを簡単に実装するための新しいスレッドクラスです。std::jthreadは、スレッドのライフサイクル管理とキャンセル機能を一体化して提供することで、スレッド管理をより簡単にしています。ここでは、std::jthreadの基本的な使い方と、キャンセル処理における利点を紹介します。

std::jthreadの基本

std::jthreadは、std::threadと同様にスレッドを起動しますが、スレッドの終了時に自動的にjoinまたはdetachを行います。また、std::stop_tokenを簡単に取り扱えるように設計されています。

以下に、std::jthreadを使用した基本的なスレッドの起動方法を示します。

#include <iostream>
#include <thread>

void simpleTask() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        std::cout << "Task running: " << i << std::endl;
    }
    std::cout << "Task completed." << std::endl;
}

int main() {
    std::jthread worker(simpleTask);
    return 0;
}

このコードでは、std::jthreadを使用してsimpleTaskを実行しています。main関数の終了時に自動的にjoinされます。

std::jthreadとキャンセル機能

std::jthreadの大きな特徴は、キャンセル機能が組み込まれていることです。これにより、スレッドを簡単にキャンセルできます。

以下の例では、std::jthreadを使用してキャンセル可能なタスクを実装しています。

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

void cancellableTask(std::stop_token stopToken) {
    while (!stopToken.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Task is running..." << std::endl;
    }
    std::cout << "Task was cancelled." << std::endl;
}

int main() {
    std::jthread worker(cancellableTask);
    std::this_thread::sleep_for(std::chrono::seconds(2));
    worker.request_stop(); // タスクの停止を要求
    return 0;
}

このコードでは、std::jthreadが自動的にstd::stop_tokenを生成し、タスクに渡します。2秒後にworker.request_stop()を呼び出すことで、タスクのキャンセルを要求しています。

std::jthreadの利点

std::jthreadを使用することで得られる利点は次のとおりです。

  1. 簡素化されたスレッド管理:std::jthreadはスレッドのライフサイクルを自動管理し、デフォルトでスレッドの終了時にjoinまたはdetachを行うため、メモリリークやデッドロックのリスクが減少します。
  2. 組み込みのキャンセル機能:std::jthreadはstd::stop_tokenとstd::stop_sourceを簡単に使用できるため、キャンセル可能なスレッドの実装が容易です。
  3. コードの可読性向上:std::jthreadを使用することで、スレッド管理のための追加コードが不要になり、コードの可読性と保守性が向上します。

これらの利点を活用することで、より効率的で安全なマルチスレッドプログラムを作成できます。次の項目では、実践例としてファイルダウンロードタスクにおけるキャンセル処理を紹介します。

実践例: ファイルダウンロードのキャンセル

非同期タスクのキャンセルは、例えばファイルダウンロードのような長時間かかる処理で特に有用です。ここでは、std::stop_tokenを使用してファイルダウンロードタスクをキャンセルする具体的な例を紹介します。

ファイルダウンロードタスクの実装

以下の例では、ファイルのダウンロードをシミュレートし、キャンセルリクエストを受け取った場合にダウンロードを中止する方法を示します。

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

// ファイルダウンロードをシミュレートする関数
void downloadFile(std::stop_token stopToken) {
    for (int i = 0; i < 100; ++i) {
        if (stopToken.stop_requested()) {
            std::cout << "Download cancelled at " << i << "% complete." << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // ダウンロードのシミュレーション
        std::cout << "Downloading... " << i << "% complete" << std::endl;
    }
    std::cout << "Download completed successfully." << std::endl;
}

int main() {
    std::jthread downloadThread(downloadFile);

    // 2秒後にダウンロードをキャンセル
    std::this_thread::sleep_for(std::chrono::seconds(2));
    downloadThread.request_stop(); // ダウンロードの停止を要求

    downloadThread.join();
    return 0;
}

この例では、downloadFile関数がダウンロードの進捗をシミュレートしており、100回のループを通じて進行状況を表示します。各ループごとにstop_requestedメソッドをチェックし、キャンセル要求があればダウンロードを中止します。main関数では、2秒後にダウンロードをキャンセルしています。

エラーハンドリングとリソース管理

キャンセル処理を行う際には、リソースの解放やエラーハンドリングも重要です。以下の例では、ダウンロード中にキャンセルが発生した場合のリソース管理について説明します。

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

// ファイルダウンロードをシミュレートする関数
void downloadFileWithErrorHandling(std::stop_token stopToken) {
    std::ofstream outputFile("downloaded_file.txt");
    if (!outputFile.is_open()) {
        std::cerr << "Failed to open file for writing." << std::endl;
        return;
    }

    for (int i = 0; i < 100; ++i) {
        if (stopToken.stop_requested()) {
            std::cout << "Download cancelled at " << i << "% complete." << std::endl;
            outputFile.close();
            // 部分的にダウンロードされたファイルを削除
            std::remove("downloaded_file.txt");
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // ダウンロードのシミュレーション
        outputFile << "Data chunk " << i << std::endl; // ダウンロードデータの書き込みシミュレーション
    }

    outputFile.close();
    std::cout << "Download completed successfully." << std::endl;
}

int main() {
    std::jthread downloadThread(downloadFileWithErrorHandling);

    // 2秒後にダウンロードをキャンセル
    std::this_thread::sleep_for(std::chrono::seconds(2));
    downloadThread.request_stop(); // ダウンロードの停止を要求

    downloadThread.join();
    return 0;
}

このコードでは、ダウンロードデータをファイルに書き込むシミュレーションを行っています。キャンセル要求が発生した場合、ファイルを閉じて部分的にダウンロードされたファイルを削除します。

これにより、キャンセルされたダウンロードタスクの後始末を適切に行い、リソースリークや不完全なファイルが残らないようにします。次の項目では、キャンセル処理時のエラーハンドリングとリソース管理についてさらに詳しく解説します。

エラーハンドリングとリソース管理

非同期タスクのキャンセル処理を実装する際には、エラーハンドリングとリソース管理が重要です。適切なエラーハンドリングを行うことで、予期しないエラーによるクラッシュを防ぎ、リソース管理を徹底することで、リソースリークを回避できます。ここでは、キャンセル処理時のエラーハンドリングとリソース管理の具体的な方法を解説します。

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

非同期タスクがキャンセルされた場合、適切にエラーを処理し、リソースを解放する必要があります。以下の例では、ファイルダウンロードタスクのキャンセル時にエラーハンドリングを行っています。

#include <iostream>
#include <thread>
#include <stop_token>
#include <fstream>
#include <stdexcept>

void downloadFileWithExceptionHandling(std::stop_token stopToken) {
    std::ofstream outputFile("downloaded_file.txt", std::ios::out | std::ios::trunc);
    if (!outputFile.is_open()) {
        std::cerr << "Failed to open file for writing." << std::endl;
        return;
    }

    try {
        for (int i = 0; i < 100; ++i) {
            if (stopToken.stop_requested()) {
                throw std::runtime_error("Download cancelled by user.");
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(50)); // ダウンロードのシミュレーション
            outputFile << "Data chunk " << i << std::endl; // ダウンロードデータの書き込みシミュレーション
        }
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        outputFile.close();
        std::remove("downloaded_file.txt");
        return;
    }

    outputFile.close();
    std::cout << "Download completed successfully." << std::endl;
}

int main() {
    std::jthread downloadThread(downloadFileWithExceptionHandling);

    // 2秒後にダウンロードをキャンセル
    std::this_thread::sleep_for(std::chrono::seconds(2));
    downloadThread.request_stop(); // ダウンロードの停止を要求

    downloadThread.join();
    return 0;
}

このコードでは、キャンセルが要求された際に例外を投げることでエラーハンドリングを行っています。catchブロック内でエラーを適切に処理し、リソースを解放しています。

リソース管理のベストプラクティス

リソース管理を徹底するためには、以下のポイントに注意する必要があります。

  1. リソースの確実な解放:タスクがキャンセルされた場合でも、すべてのリソースが確実に解放されるようにします。これには、ファイル、メモリ、ネットワークリソースなどが含まれます。
  2. スコープガードの活用:スコープガード(scope guard)パターンを使用することで、スコープを抜ける際にリソースを自動的に解放できます。C++11以降では、std::unique_ptrとカスタムデリータを組み合わせてスコープガードを実現できます。

以下に、スコープガードを使用したリソース管理の例を示します。

#include <iostream>
#include <thread>
#include <stop_token>
#include <fstream>
#include <memory>

class FileGuard {
public:
    explicit FileGuard(std::ofstream& file) : file_(file) {}
    ~FileGuard() {
        if (file_.is_open()) {
            file_.close();
            std::remove("downloaded_file.txt");
        }
    }
private:
    std::ofstream& file_;
};

void downloadFileWithScopeGuard(std::stop_token stopToken) {
    std::ofstream outputFile("downloaded_file.txt", std::ios::out | std::ios::trunc);
    if (!outputFile.is_open()) {
        std::cerr << "Failed to open file for writing." << std::endl;
        return;
    }

    FileGuard guard(outputFile); // スコープガードを使用

    for (int i = 0; i < 100; ++i) {
        if (stopToken.stop_requested()) {
            std::cout << "Download cancelled at " << i << "% complete." << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // ダウンロードのシミュレーション
        outputFile << "Data chunk " << i << std::endl; // ダウンロードデータの書き込みシミュレーション
    }

    outputFile.close(); // 正常に終了した場合、ファイルを閉じる
    std::cout << "Download completed successfully." << std::endl;
}

int main() {
    std::jthread downloadThread(downloadFileWithScopeGuard);

    // 2秒後にダウンロードをキャンセル
    std::this_thread::sleep_for(std::chrono::seconds(2));
    downloadThread.request_stop(); // ダウンロードの停止を要求

    downloadThread.join();
    return 0;
}

この例では、FileGuardクラスを使用して、スコープを抜ける際にファイルを閉じ、部分的にダウンロードされたファイルを削除しています。これにより、リソースが確実に解放されることが保証されます。

適切なエラーハンドリングとリソース管理を実装することで、非同期タスクのキャンセル処理をより安全かつ効率的に行うことができます。次の項目では、並行タスクのキャンセル処理について解説します。

応用例: 並行タスクのキャンセル

複数の並行タスクを実行する際にも、std::stop_tokenを使用することで効率的にキャンセル処理を実装できます。ここでは、複数のタスクを並行して実行し、それらを一括してキャンセルする方法を具体的な例を通じて解説します。

複数タスクのキャンセル

複数のタスクを並行して実行し、同じstd::stop_sourceを使用してこれらを一括してキャンセルする方法を紹介します。

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

// 個々のタスクをシミュレートする関数
void workerTask(std::stop_token stopToken, int taskId) {
    for (int i = 0; i < 100; ++i) {
        if (stopToken.stop_requested()) {
            std::cout << "Task " << taskId << " was cancelled at " << i << "% complete." << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        std::cout << "Task " << taskId << " running: " << i << "% complete" << std::endl;
    }
    std::cout << "Task " << taskId << " completed successfully." << std::endl;
}

int main() {
    std::stop_source stopSource;

    // 複数のタスクを起動
    std::vector<std::jthread> threads;
    for (int i = 1; i <= 3; ++i) {
        threads.emplace_back(workerTask, stopSource.get_token(), i);
    }

    // 2秒後に全タスクをキャンセル
    std::this_thread::sleep_for(std::chrono::seconds(2));
    stopSource.request_stop();

    // すべてのスレッドが終了するのを待つ
    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

このコードでは、3つのタスクを並行して実行しています。各タスクはworkerTask関数内でstopTokenを使用してキャンセル要求をチェックしています。2秒後にstopSourceのrequest_stopメソッドを呼び出すことで、すべてのタスクが一括してキャンセルされます。

タスク間の同期と協調

並行タスク間での同期と協調を実現するためには、std::stop_tokenをうまく活用する必要があります。以下の例では、複数のタスクが協調して動作し、キャンセル要求が発生した場合に一斉に停止する方法を示します。

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

std::mutex io_mutex;

void synchronizedTask(std::stop_token stopToken, int taskId) {
    for (int i = 0; i < 100; ++i) {
        if (stopToken.stop_requested()) {
            std::lock_guard<std::mutex> lock(io_mutex);
            std::cout << "Task " << taskId << " was cancelled at " << i << "% complete." << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        {
            std::lock_guard<std::mutex> lock(io_mutex);
            std::cout << "Task " << taskId << " running: " << i << "% complete" << std::endl;
        }
    }
    std::lock_guard<std::mutex> lock(io_mutex);
    std::cout << "Task " << taskId << " completed successfully." << std::endl;
}

int main() {
    std::stop_source stopSource;

    // 複数のタスクを起動
    std::vector<std::jthread> threads;
    for (int i = 1; i <= 3; ++i) {
        threads.emplace_back(synchronizedTask, stopSource.get_token(), i);
    }

    // 2秒後に全タスクをキャンセル
    std::this_thread::sleep_for(std::chrono::seconds(2));
    stopSource.request_stop();

    // すべてのスレッドが終了するのを待つ
    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

この例では、synchronizedTask関数内でstd::mutexを使用して標準出力へのアクセスを同期しています。これにより、複数のタスクが並行して標準出力を利用しても、メッセージが混ざらないようにしています。また、キャンセル要求が発生した場合に、すべてのタスクが一斉に停止するようになっています。

キャンセル可能な並行タスクの応用例

実際のアプリケーションでは、複数の並行タスクが協調して動作するケースが多々あります。以下に、キャンセル可能な並行タスクの応用例として、ファイルのダウンロードと処理を並行して行い、キャンセル機能を実装した例を示します。

#include <iostream>
#include <thread>
#include <stop_token>
#include <vector>
#include <mutex>
#include <fstream>
#include <atomic>

std::mutex io_mutex;

void downloadTask(std::stop_token stopToken, std::atomic<bool>& downloadCompleted) {
    std::ofstream outputFile("downloaded_file.txt", std::ios::out | std::ios::trunc);
    if (!outputFile.is_open()) {
        std::cerr << "Failed to open file for writing." << std::endl;
        return;
    }

    for (int i = 0; i < 100; ++i) {
        if (stopToken.stop_requested()) {
            std::lock_guard<std::mutex> lock(io_mutex);
            std::cout << "Download task was cancelled at " << i << "% complete." << std::endl;
            outputFile.close();
            std::remove("downloaded_file.txt");
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        outputFile << "Data chunk " << i << std::endl;
    }
    outputFile.close();
    downloadCompleted.store(true);
    std::lock_guard<std::mutex> lock(io_mutex);
    std::cout << "Download task completed successfully." << std::endl;
}

void processingTask(std::stop_token stopToken, std::atomic<bool>& downloadCompleted) {
    while (!downloadCompleted.load()) {
        if (stopToken.stop_requested()) {
            std::lock_guard<std::mutex> lock(io_mutex);
            std::cout << "Processing task was cancelled before download completed." << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::lock_guard<std::mutex> lock(io_mutex);
        std::cout << "Waiting for download to complete..." << std::endl;
    }

    std::ifstream inputFile("downloaded_file.txt");
    if (!inputFile.is_open()) {
        std::cerr << "Failed to open file for reading." << std::endl;
        return;
    }

    std::string line;
    while (std::getline(inputFile, line)) {
        if (stopToken.stop_requested()) {
            std::lock_guard<std::mutex> lock(io_mutex);
            std::cout << "Processing task was cancelled during processing." << std::endl;
            inputFile.close();
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        std::lock_guard<std::mutex> lock(io_mutex);
        std::cout << "Processing: " << line << std::endl;
    }
    inputFile.close();
    std::lock_guard<std::mutex> lock(io_mutex);
    std::cout << "Processing task completed successfully." << std::endl;
}

int main() {
    std::stop_source stopSource;
    std::atomic<bool> downloadCompleted(false);

    // ダウンロードタスクと処理タスクを起動
    std::jthread downloadThread(downloadTask, stopSource.get_token(), std::ref(downloadCompleted));
    std::jthread processingThread(processingTask, stopSource.get_token(), std::ref(downloadCompleted));

    // 2秒後に全タスクをキャンセル
    std::this_thread::sleep_for(std::chrono::seconds(2));
    stopSource.request_stop();

    downloadThread.join();
    processingThread.join();

    return 0;
}

この例では、ファイルのダウンロードタスクとそのファイルを処理するタスクを並行して実行しています。ダウンロードタスクはキャンセル要求を受け取ると、中断してファイルを削除します。処理タスクは、ダウンロードの完了を待ちつつ、キャンセル要求が発生した場合には適切に中断します。

これにより、並行して実行されるタスク間の協調とキャンセル処理を効率的に管理できます。次の項目では、本記事のまとめを行います。

まとめ

C++20で導入されたstd::stop_tokenとstd::jthreadを利用することで、非同期タスクのキャンセル処理が大幅に簡素化され、効率的なプログラムの設計が可能になりました。本記事では、以下のポイントについて解説しました。

  • std::stop_tokenの概要:キャンセルリクエストを伝えるためのトークンで、非同期タスクの中断を容易にします。
  • 非同期タスクの実装例:基本的な非同期タスクの実装と、キャンセル機能を追加する方法を紹介しました。
  • std::stop_tokenを使ったキャンセル処理:キャンセルリクエストをチェックする具体的な手順と、コールバック関数の利用方法を説明しました。
  • std::stop_sourceとの連携:キャンセルリクエストを発行するためのstd::stop_sourceとの連携方法を解説しました。
  • キャンセル可能な非同期タスクの設計:キャンセルポイントの設定やリソース管理のベストプラクティスを紹介しました。
  • std::jthreadの活用:std::jthreadを使用してキャンセル可能なスレッドを簡単に実装する方法を説明しました。
  • 実践例: ファイルダウンロードのキャンセル:ファイルダウンロードタスクのキャンセル処理を具体的に実装しました。
  • エラーハンドリングとリソース管理:キャンセル処理時のエラーハンドリングとリソース管理の重要性を解説しました。
  • 応用例: 並行タスクのキャンセル:複数の並行タスクをキャンセルする具体的な例を紹介しました。

これらの技術を駆使することで、複雑な非同期処理でも柔軟かつ安全にキャンセル機能を実装でき、プログラムの応答性とユーザー体験を向上させることができます。今後の開発において、ぜひstd::stop_tokenとstd::jthreadを活用してみてください。

コメント

コメントする

目次