C++での非同期プログラミング: std::futureとstd::promiseを使った効果的なタスク管理法

C++11の登場により、非同期プログラミングのための強力なツールが提供されました。その中でも、std::futureとstd::promiseは、非同期タスクの管理と結果の取得を容易にします。本記事では、これらの機能を用いた基本的な非同期プログラミングの手法から、複雑なタスク管理までを包括的に解説します。非同期プログラミングの利点や実践的な応用方法について学び、パフォーマンスを向上させるテクニックを身につけましょう。

目次

std::futureとstd::promiseの基本概念

C++の非同期プログラミングにおいて、std::futureとstd::promiseは重要な役割を果たします。std::promiseは値を設定する手段を提供し、std::futureはその値を非同期的に取得する手段を提供します。このセクションでは、それぞれの役割と基本的な使い方について説明します。

std::promiseの基本

std::promiseは、非同期タスクが完了したときに結果を設定するためのオブジェクトです。以下のコード例は、std::promiseの基本的な使い方を示しています。

#include <iostream>
#include <future>

void set_value(std::promise<int>& prom) {
    prom.set_value(10);
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    std::thread t(set_value, std::ref(prom));
    t.join();

    std::cout << "The value is: " << fut.get() << std::endl;
    return 0;
}

std::futureの基本

std::futureは、非同期タスクの結果を取得するためのオブジェクトです。上記のコード例で示したように、std::futureはstd::promiseから生成され、非同期的に設定された値を取得することができます。

std::futureとstd::promiseを使った基本的な非同期タスク

std::futureとstd::promiseを利用することで、C++で簡単に非同期タスクを実装することができます。ここでは、基本的な非同期タスクの実装方法を具体的なコード例とともに説明します。

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

以下のコード例では、std::promiseとstd::futureを使って非同期タスクを実行し、結果を取得する方法を示します。

#include <iostream>
#include <thread>
#include <future>

// 非同期に実行する関数
void async_task(std::promise<int>& prom) {
    // ここで何らかの時間のかかる処理を行う
    std::this_thread::sleep_for(std::chrono::seconds(2));
    // 処理結果をstd::promiseに設定
    prom.set_value(42);
}

int main() {
    // std::promiseオブジェクトを作成
    std::promise<int> prom;
    // std::futureオブジェクトをstd::promiseから取得
    std::future<int> fut = prom.get_future();

    // 非同期タスクを実行するスレッドを作成
    std::thread t(async_task, std::ref(prom));

    // メインスレッドで他の処理を行う
    std::cout << "Waiting for the result..." << std::endl;

    // 非同期タスクの結果を取得
    int result = fut.get();
    std::cout << "The result is: " << result << std::endl;

    // スレッドを終了
    t.join();

    return 0;
}

コードの解説

  1. std::promiseとstd::futureの作成:
  • std::promise<int> prom; でpromiseオブジェクトを作成し、prom.get_future()で対応するfutureオブジェクトを取得します。
  1. 非同期タスクの実行:
  • 新しいスレッドを作成し、非同期に実行する関数async_taskを実行します。この関数は、時間のかかる処理を行った後、結果をpromiseオブジェクトに設定します。
  1. 結果の取得:
  • メインスレッドでは、futureオブジェクトのget()メソッドを呼び出して結果を取得します。このメソッドは、非同期タスクが完了するまでブロックされます。

このように、std::futureとstd::promiseを使うことで、非同期タスクの実装が簡単に行えます。

std::asyncを使った非同期タスクの管理

C++標準ライブラリには、非同期タスクをより簡単に管理するための便利な関数としてstd::asyncが含まれています。std::asyncを使うと、手動でスレッドを管理する必要がなくなり、非同期タスクの実装がさらに簡単になります。

std::asyncの基本的な使い方

以下のコード例では、std::asyncを使って非同期タスクを実行し、その結果を取得する方法を示します。

#include <iostream>
#include <future>

// 非同期に実行する関数
int async_task() {
    // ここで何らかの時間のかかる処理を行う
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 42;
}

int main() {
    // std::asyncを使って非同期タスクを実行
    std::future<int> fut = std::async(std::launch::async, async_task);

    // メインスレッドで他の処理を行う
    std::cout << "Waiting for the result..." << std::endl;

    // 非同期タスクの結果を取得
    int result = fut.get();
    std::cout << "The result is: " << result << std::endl;

    return 0;
}

コードの解説

  1. 非同期タスクの実行:
  • std::async(std::launch::async, async_task)を使って非同期タスクを実行します。この呼び出しは、新しいスレッドでasync_task関数を実行し、結果を返すfutureオブジェクトを返します。
  1. 結果の取得:
  • メインスレッドでは、futureオブジェクトのget()メソッドを呼び出して結果を取得します。このメソッドは、非同期タスクが完了するまでブロックされます。

std::asyncのオプション

std::asyncにはいくつかのオプションがあります。例えば、std::launch::deferredを指定すると、タスクはget()が呼び出されたときに実行されます。以下はその例です。

int main() {
    // std::asyncを使って非同期タスクを実行(遅延実行)
    std::future<int> fut = std::async(std::launch::deferred, async_task);

    // メインスレッドで他の処理を行う
    std::cout << "Waiting for the result..." << std::endl;

    // 非同期タスクの結果を取得(ここで実行される)
    int result = fut.get();
    std::cout << "The result is: " << result << std::endl;

    return 0;
}

std::asyncを使うことで、非同期タスクの実行が簡単になり、コードがより読みやすく、メンテナンスしやすくなります。

タスクのキャンセルとエラーハンドリング

非同期プログラミングでは、タスクのキャンセルやエラー処理が重要です。C++では、std::futureとstd::promiseを使って、これらの状況を適切に管理することができます。

タスクのキャンセル

C++標準ライブラリには直接的なタスクキャンセルの機能はありませんが、キャンセルフラグを使ってタスクを間接的にキャンセルする方法があります。

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

// 非同期に実行する関数
void async_task(std::promise<void>& prom, std::atomic<bool>& cancel_flag) {
    // キャンセルフラグをチェックしながらループ
    for (int i = 0; i < 10; ++i) {
        if (cancel_flag.load()) {
            std::cout << "Task was cancelled." << std::endl;
            return;
        }
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "Processing " << i << std::endl;
    }
    prom.set_value();
}

int main() {
    std::promise<void> prom;
    std::future<void> fut = prom.get_future();
    std::atomic<bool> cancel_flag{false};

    std::thread t(async_task, std::ref(prom), std::ref(cancel_flag));

    // キャンセルをシミュレート
    std::this_thread::sleep_for(std::chrono::seconds(3));
    cancel_flag.store(true);

    fut.wait();
    t.join();

    return 0;
}

エラーハンドリング

非同期タスクでエラーが発生した場合、std::promiseとstd::futureを使って例外を伝搬させることができます。

#include <iostream>
#include <thread>
#include <future>
#include <stdexcept>

// 非同期に実行する関数
void async_task(std::promise<int>& prom) {
    try {
        // ここで何らかの時間のかかる処理を行う
        std::this_thread::sleep_for(std::chrono::seconds(2));
        throw std::runtime_error("Something went wrong");
        prom.set_value(42);
    } catch (...) {
        prom.set_exception(std::current_exception());
    }
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    std::thread t(async_task, std::ref(prom));

    try {
        int result = fut.get();
        std::cout << "The result is: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }

    t.join();

    return 0;
}

コードの解説

  1. タスクのキャンセル:
  • キャンセルフラグ(std::atomic<bool>)を使って、タスクが実行中にキャンセルされたかどうかをチェックします。
  1. エラーハンドリング:
  • 非同期タスク内で例外が発生した場合、prom.set_exception(std::current_exception())を使って例外をpromiseに設定し、futureでその例外をキャッチします。

これらのテクニックを使うことで、非同期プログラミングにおけるタスクのキャンセルとエラーハンドリングを適切に管理できます。

複数の非同期タスクの同期

複数の非同期タスクを効率的に管理し、同期させる方法は、非同期プログラミングにおいて重要なテクニックです。C++では、複数のstd::futureを使ってこれを実現できます。

std::futureの複数利用

以下のコード例では、複数の非同期タスクを実行し、その結果を取得する方法を示します。

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

// 非同期に実行する関数
int async_task(int id) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return id * 10;
}

int main() {
    std::vector<std::future<int>> futures;

    // 複数の非同期タスクを起動
    for (int i = 0; i < 5; ++i) {
        futures.push_back(std::async(std::launch::async, async_task, i));
    }

    // 結果を取得
    for (auto& fut : futures) {
        int result = fut.get();
        std::cout << "Result: " << result << std::endl;
    }

    return 0;
}

コードの解説

  1. 複数の非同期タスクの起動:
  • std::asyncを使って複数の非同期タスクを起動し、それぞれのfutureオブジェクトをstd::vectorに格納します。
  1. 結果の取得:
  • 各futureオブジェクトのget()メソッドを呼び出して、非同期タスクの結果を取得します。

std::when_allを使ったタスクの同期

C++20では、std::when_allを使って複数の非同期タスクを同期させることができます。以下はその例です。

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

// 非同期に実行する関数
int async_task(int id) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return id * 10;
}

int main() {
    std::vector<std::future<int>> futures;

    // 複数の非同期タスクを起動
    for (int i = 0; i < 5; ++i) {
        futures.push_back(std::async(std::launch::async, async_task, i));
    }

    // std::when_allでタスクを同期
    auto all_futures = std::when_all(futures.begin(), futures.end());

    // 結果を取得
    for (auto& fut : all_futures.get()) {
        int result = fut.get();
        std::cout << "Result: " << result << std::endl;
    }

    return 0;
}

コードの解説

  1. std::when_allの使用:
  • std::when_allを使って、複数のfutureオブジェクトがすべて完了するのを待ちます。
  1. 結果の取得:
  • 各futureオブジェクトのget()メソッドを呼び出して、非同期タスクの結果を取得します。

これらのテクニックを使うことで、複数の非同期タスクを効率的に同期させることができ、より複雑な非同期プログラムを実装することができます。

応用例: 非同期タスクの並列実行

非同期タスクの並列実行は、複雑なプログラムにおいてパフォーマンスを大幅に向上させることができます。このセクションでは、非同期タスクを並列に実行する具体的な応用例を紹介します。

並列に実行するタスクの例

以下のコード例では、複数のファイルを並列に処理するシナリオを示します。このシナリオでは、各ファイルの内容を読み込み、その内容を処理する非同期タスクを並列に実行します。

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <future>

// ファイルを読み込む関数
std::string read_file(const std::string& filename) {
    std::ifstream file(filename);
    std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    return content;
}

// ファイルの内容を処理する関数
void process_file(const std::string& filename) {
    std::string content = read_file(filename);
    std::cout << "Processing file: " << filename << ", size: " << content.size() << " bytes" << std::endl;
}

int main() {
    std::vector<std::string> files = {"file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"};
    std::vector<std::future<void>> futures;

    // 各ファイルの処理を並列に実行
    for (const auto& file : files) {
        futures.push_back(std::async(std::launch::async, process_file, file));
    }

    // すべてのタスクが完了するのを待つ
    for (auto& fut : futures) {
        fut.get();
    }

    std::cout << "All files processed." << std::endl;
    return 0;
}

コードの解説

  1. ファイルの読み込み:
  • read_file関数は、指定されたファイルを読み込み、その内容を文字列として返します。
  1. ファイルの処理:
  • process_file関数は、ファイルを読み込み、その内容を処理します。この場合、単にファイルのサイズを出力しています。
  1. 並列実行:
  • メイン関数で、各ファイルの処理をstd::asyncを使って並列に実行します。各非同期タスクはfuturesベクターに格納されます。
  1. タスクの同期:
  • すべてのタスクが完了するのを待つために、各futureのget()メソッドを呼び出します。

並列実行のメリット

  • パフォーマンスの向上: 複数のタスクを並列に実行することで、処理時間を大幅に短縮できます。
  • リソースの効率的な利用: 複数のCPUコアを効率的に利用することで、システムのリソースを最大限に活用できます。

並列実行は、特に大量のデータ処理や計算を伴うタスクにおいて有効です。この技術を活用することで、アプリケーションのパフォーマンスを向上させることができます。

パフォーマンスの最適化

非同期プログラミングを利用する際には、パフォーマンスの最適化が重要です。ここでは、C++で非同期タスクのパフォーマンスを最適化するためのいくつかの手法を紹介します。

スレッドプールの利用

複数の非同期タスクを効率的に管理するために、スレッドプールを利用することが推奨されます。スレッドプールを使用することで、新しいタスクごとにスレッドを生成するオーバーヘッドを削減できます。

#include <iostream>
#include <vector>
#include <thread>
#include <future>
#include <queue>
#include <functional>
#include <condition_variable>

// スレッドプールクラス
class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();

    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;
};

inline ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
    for(size_t i = 0; i < numThreads; ++i) {
        workers.emplace_back(
            [this] {
                for(;;) {
                    std::function<void()> task;

                    {
                        std::unique_lock<std::mutex> lock(this->queueMutex);
                        this->condition.wait(lock, [this]{ return this->stop || !this->tasks.empty(); });
                        if(this->stop && this->tasks.empty())
                            return;
                        task = std::move(this->tasks.front());
                        this->tasks.pop();
                    }

                    task();
                }
            }
        );
    }
}

template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
    using returnType = typename std::result_of<F(Args...)>::type;

    auto task = std::make_shared<std::packaged_task<returnType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));

    std::future<returnType> res = task->get_future();
    {
        std::unique_lock<std::mutex> lock(queueMutex);

        if(stop)
            throw std::runtime_error("enqueue on stopped ThreadPool");

        tasks.emplace([task](){ (*task)(); });
    }
    condition.notify_one();
    return res;
}

inline ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        stop = true;
    }
    condition.notify_all();
    for(std::thread &worker: workers)
        worker.join();
}

int main() {
    ThreadPool pool(4);

    std::vector<std::future<int>> results;

    for(int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
                std::this_thread::sleep_for(std::chrono::seconds(1));
                return i * i;
            })
        );
    }

    for(auto && result: results)
        std::cout << result.get() << ' ';
    std::cout << std::endl;

    return 0;
}

std::asyncの利用方法

std::asyncの利用には注意が必要です。デフォルトでは、std::asyncは非同期タスクを実行するために新しいスレッドを作成するか、呼び出し元のスレッドで実行するかを決定します。パフォーマンスを最適化するために、std::launch::asyncを明示的に指定して新しいスレッドで実行させることができます。

auto result = std::async(std::launch::async, [] {
    // 非同期タスクの内容
});

結果の取得タイミング

非同期タスクの結果を取得するタイミングも重要です。早すぎると非同期のメリットが失われ、遅すぎると不要な遅延が発生する可能性があります。適切なタイミングでget()メソッドを呼び出して結果を取得しましょう。

負荷分散

複数のタスクを並行して実行する場合、システムの負荷を均等に分散することが重要です。スレッドプールを利用して、タスクを均等に分配し、システムリソースを最大限に活用しましょう。

これらの最適化手法を取り入れることで、非同期プログラミングのパフォーマンスを大幅に向上させることができます。

実践演習: std::futureとstd::promiseを使ったプロジェクト

ここでは、std::futureとstd::promiseを使った実践的なプロジェクトを通じて、非同期プログラミングの理解を深めましょう。このプロジェクトでは、複数のウェブページを非同期にダウンロードし、その内容を処理する例を紹介します。

プロジェクト概要

複数のURLからウェブページの内容を非同期にダウンロードし、その内容を解析して、特定のキーワードの出現回数をカウントします。各ダウンロードタスクは非同期に実行され、ダウンロードが完了したら解析タスクが実行されます。

コード例

#include <iostream>
#include <vector>
#include <string>
#include <future>
#include <curl/curl.h>

// ダウンロードしたデータを格納する構造体
struct DownloadedData {
    std::string url;
    std::string content;
};

// CURLのコールバック関数
size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
    ((std::string*)userp)->append((char*)contents, size * nmemb);
    return size * nmemb;
}

// URLからデータをダウンロードする関数
DownloadedData download_url(const std::string& url) {
    CURL* curl;
    CURLcode res;
    std::string readBuffer;

    curl = curl_easy_init();
    if(curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
        res = curl_easy_perform(curl);
        curl_easy_cleanup(curl);
    }

    return {url, readBuffer};
}

// キーワードの出現回数をカウントする関数
int count_keyword(const std::string& content, const std::string& keyword) {
    int count = 0;
    size_t pos = content.find(keyword);
    while (pos != std::string::npos) {
        count++;
        pos = content.find(keyword, pos + keyword.length());
    }
    return count;
}

int main() {
    // ダウンロードするURLのリスト
    std::vector<std::string> urls = {
        "http://example.com",
        "http://example.org",
        "http://example.net"
    };

    // ダウンロードタスクを非同期に実行
    std::vector<std::future<DownloadedData>> futures;
    for (const auto& url : urls) {
        futures.push_back(std::async(std::launch::async, download_url, url));
    }

    // キーワードの出現回数をカウント
    std::string keyword = "example";
    for (auto& fut : futures) {
        DownloadedData data = fut.get();
        int count = count_keyword(data.content, keyword);
        std::cout << "URL: " << data.url << ", Keyword '" << keyword << "' Count: " << count << std::endl;
    }

    return 0;
}

コードの解説

  1. CURLの設定:
  • WriteCallback関数を使って、ダウンロードしたデータを文字列に格納します。
  1. 非同期タスクの実行:
  • std::asyncを使って、URLからデータをダウンロードする非同期タスクを実行します。
  1. 結果の取得と解析:
  • 各非同期タスクの結果をget()メソッドで取得し、その内容を解析してキーワードの出現回数をカウントします。

プロジェクトのメリット

  • 並列ダウンロード: 複数のURLからのデータダウンロードを並列に行うことで、全体の処理時間を短縮します。
  • 非同期解析: ダウンロードが完了したデータをすぐに解析することで、効率的な処理を実現します。

このプロジェクトを通じて、std::futureとstd::promiseを使った非同期プログラミングの実践的な応用方法を学び、複雑なタスクの効率的な管理方法を理解できるでしょう。

まとめ

本記事では、C++におけるstd::futureとstd::promiseを用いた非同期プログラミングの基本から応用までを詳しく解説しました。以下に、本記事のポイントをまとめます。

  • std::futureとstd::promiseの基本概念: これらのクラスの役割と基本的な使い方を理解しました。
  • 基本的な非同期タスクの実装: コード例を通じて、非同期タスクの基本的な実装方法を学びました。
  • std::asyncを使ったタスク管理: std::asyncを利用して、より簡潔に非同期タスクを管理する方法を解説しました。
  • タスクのキャンセルとエラーハンドリング: 非同期タスクにおけるキャンセル方法やエラーハンドリングの実装方法を学びました。
  • 複数の非同期タスクの同期: 複数の非同期タスクを効率的に同期させる方法について理解しました。
  • 応用例: 非同期タスクの並列実行: 非同期タスクを並列に実行する具体的な例を通じて、パフォーマンス向上の方法を学びました。
  • パフォーマンスの最適化: スレッドプールの利用や適切なstd::asyncの利用方法について解説し、非同期プログラミングのパフォーマンスを最適化する手法を紹介しました。
  • 実践演習: 実践的なプロジェクト例を通じて、非同期プログラミングの応用方法を理解しました。

これらの知識と技術を駆使して、C++での非同期プログラミングを効果的に実践し、複雑なタスクを効率的に管理できるようになるでしょう。この記事が、非同期プログラミングの理解と実践に役立つことを願っています。

コメント

コメントする

目次