C++での非同期プログラミングと例外処理の組み合わせを徹底解説

非同期プログラミングと例外処理は、現代のソフトウェア開発において非常に重要な技術です。特にC++では、高性能なアプリケーションを構築するために非同期処理が不可欠ですが、それに伴う例外処理も複雑さを増します。本記事では、非同期プログラミングと例外処理の基本概念から始め、C++での具体的な実装方法や課題、そして効果的な組み合わせのパターンについて詳しく解説します。読者の皆さんが、より堅牢で効率的なC++プログラムを作成できるようになることを目指しています。

目次
  1. 非同期プログラミングの基礎
    1. パフォーマンスの向上
    2. ユーザー体験の向上
    3. リソースの効率的な利用
  2. C++における非同期プログラミングの手法
    1. std::async
    2. std::thread
    3. Boost.Asio
  3. 例外処理の基礎
    1. 例外の発生と捕捉
    2. 標準例外クラス
    3. カスタム例外クラス
  4. C++における例外処理の実装
    1. 基本的な例外処理の構造
    2. 複数の例外を捕捉する
    3. 標準ライブラリの例外クラス
    4. カスタム例外クラスの作成
    5. リソース管理とRAII
  5. 非同期プログラミングと例外処理の組み合わせの課題
    1. 例外の捕捉と伝播
    2. スレッドの管理
    3. 例外の非同期処理
  6. 非同期プログラミングでの例外処理のパターン
    1. 未来オブジェクト(Future)を用いた例外処理
    2. プロミス(Promise)と未来オブジェクト(Future)を組み合わせる
    3. 継続タスク(Continuation Task)を用いた例外処理
    4. タスクベースの例外処理
  7. 実践的なコード例
    1. 例:非同期タスクでのファイル読み込みと例外処理
    2. 例:複数の非同期タスクと例外処理
    3. 例:非同期タスクと継続タスクでの例外処理
  8. ベストプラクティス
    1. タスクの設計と分割
    2. 例外の明示的な処理
    3. リソース管理の徹底
    4. ログとデバッグの徹底
    5. スレッドプールの利用
  9. 応用例と演習問題
    1. 応用例1: 非同期ファイルダウンロードとエラーハンドリング
    2. 応用例2: 非同期データ処理と例外の伝播
    3. 演習問題
  10. まとめ

非同期プログラミングの基礎

非同期プログラミングとは、プログラムの実行を中断することなく、複数のタスクを同時に処理する技術です。これにより、I/O操作やネットワーク通信などの待ち時間が発生する処理を効率的に行うことが可能になります。非同期プログラミングの主な利点には、次のようなものがあります。

パフォーマンスの向上

非同期処理を利用することで、CPUの使用率を最大化し、システム全体のパフォーマンスを向上させることができます。特に、複数のI/O操作を同時に処理する際に有効です。

ユーザー体験の向上

ユーザーインターフェースが応答を維持するため、アプリケーションの使用感が向上します。例えば、ファイルのダウンロード中でもユーザーは他の操作を行うことができます。

リソースの効率的な利用

非同期処理は、リソースの待ち時間を他のタスクで有効に活用するため、全体的なリソースの利用効率が改善されます。

非同期プログラミングを理解するためには、非同期タスク、コールバック、プロミス(Promise)、フューチャー(Future)といった概念を把握することが重要です。次のセクションでは、C++における具体的な非同期プログラミングの手法について詳しく説明します。

C++における非同期プログラミングの手法

C++では、非同期プログラミングを実現するためにいくつかの主要な手法があります。これらの手法を理解することで、効率的かつ効果的に非同期処理を活用できるようになります。

std::async

C++11で導入されたstd::asyncは、非同期タスクを簡単に実行するための標準ライブラリの一部です。指定した関数を別のスレッドで非同期に実行し、その結果をstd::futureオブジェクトを介して取得することができます。

#include <iostream>
#include <future>

int compute() {
    // 時間のかかる計算処理
    return 42;
}

int main() {
    std::future<int> result = std::async(std::launch::async, compute);
    std::cout << "計算結果: " << result.get() << std::endl;
    return 0;
}

std::thread

std::threadを使用すると、明示的に新しいスレッドを作成してタスクを実行できます。これは、より低レベルの制御を必要とする場合に役立ちます。

#include <iostream>
#include <thread>

void task() {
    std::cout << "スレッド内で実行されるタスク" << std::endl;
}

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

Boost.Asio

Boost.Asioは、非同期I/Oを提供する強力なライブラリで、ネットワークプログラミングなどに広く使われています。複雑な非同期操作を効率的に扱うための多様な機能を備えています。

#include <boost/asio.hpp>
#include <iostream>

void print(const boost::system::error_code&) {
    std::cout << "タイマー発火" << std::endl;
}

int main() {
    boost::asio::io_context io;
    boost::asio::steady_timer timer(io, boost::asio::chrono::seconds(1));
    timer.async_wait(print);
    io.run();
    return 0;
}

これらの手法を組み合わせることで、C++で強力な非同期プログラミングを実現できます。次のセクションでは、例外処理の基礎について説明します。

例外処理の基礎

例外処理は、プログラムの実行中に発生するエラーや異常状態を適切に処理するための重要な技術です。C++では、例外処理を用いることで、エラーが発生した際にプログラムを安全に終了させたり、エラーを修正して続行することが可能です。

例外の発生と捕捉

C++では、trycatch、およびthrowキーワードを使用して例外処理を行います。tryブロック内で発生した例外は、catchブロックで捕捉されます。

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::runtime_error("例外が発生しました");
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

標準例外クラス

C++標準ライブラリには、さまざまな例外クラスが用意されています。これらを使用することで、一般的なエラーを効率的に処理できます。

  • std::exception: すべての標準例外クラスの基底クラス。
  • std::runtime_error: 実行時エラーを表す。
  • std::logic_error: 論理エラーを表す。
#include <iostream>
#include <stdexcept>

void checkValue(int value) {
    if (value < 0) {
        throw std::invalid_argument("負の値は無効です");
    }
}

int main() {
    try {
        checkValue(-1);
    } catch (const std::invalid_argument& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

カスタム例外クラス

独自の例外クラスを作成することもできます。これにより、特定の状況に対応したエラーメッセージやデータを提供できます。

#include <iostream>
#include <exception>

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "カスタム例外が発生しました";
    }
};

int main() {
    try {
        throw MyException();
    } catch (const MyException& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

例外処理を理解することで、プログラムの堅牢性を大幅に向上させることができます。次のセクションでは、C++における具体的な例外処理の実装方法について詳しく説明します。

C++における例外処理の実装

C++では、例外処理を効果的に実装するために、いくつかの基本的な手法とパターンがあります。これらの方法を理解することで、予期しないエラーに対するプログラムの堅牢性を高めることができます。

基本的な例外処理の構造

C++の例外処理は、trycatch、およびthrowキーワードを使用します。tryブロック内で例外が発生した場合、対応するcatchブロックでその例外を捕捉して処理します。

#include <iostream>
#include <stdexcept>

void riskyOperation() {
    throw std::runtime_error("重大なエラーが発生しました");
}

int main() {
    try {
        riskyOperation();
    } catch (const std::runtime_error& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

複数の例外を捕捉する

複数の異なるタイプの例外を捕捉するために、複数のcatchブロックを使用することができます。

#include <iostream>
#include <stdexcept>

void checkValue(int value) {
    if (value < 0) {
        throw std::invalid_argument("負の値は無効です");
    } else if (value == 0) {
        throw std::logic_error("値がゼロです");
    }
}

int main() {
    try {
        checkValue(0);
    } catch (const std::invalid_argument& e) {
        std::cerr << "無効な引数: " << e.what() << std::endl;
    } catch (const std::logic_error& e) {
        std::cerr << "論理エラー: " << e.what() << std::endl;
    }
    return 0;
}

標準ライブラリの例外クラス

C++標準ライブラリには、多くの例外クラスが用意されています。これらを活用することで、一般的なエラーを簡単に処理することができます。

  • std::exception: すべての標準例外クラスの基底クラス
  • std::runtime_error: 実行時エラーを表す
  • std::logic_error: 論理エラーを表す

カスタム例外クラスの作成

独自の例外クラスを作成することで、特定のエラー条件に対応した例外処理を行うことができます。

#include <iostream>
#include <exception>

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "カスタム例外が発生しました";
    }
};

int main() {
    try {
        throw MyException();
    } catch (const MyException& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

リソース管理とRAII

リソースの管理にはRAII(Resource Acquisition Is Initialization)パターンを使用します。RAIIを使用することで、例外が発生してもリソースが適切に解放されるようにできます。

#include <iostream>
#include <fstream>
#include <stdexcept>

void readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        throw std::runtime_error("ファイルを開けませんでした");
    }
    // ファイル処理
}

int main() {
    try {
        readFile("example.txt");
    } catch (const std::runtime_error& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

これらの例外処理の技術を理解することで、C++プログラムの信頼性と安全性を高めることができます。次のセクションでは、非同期プログラミングと例外処理の組み合わせに伴う課題について説明します。

非同期プログラミングと例外処理の組み合わせの課題

非同期プログラミングと例外処理を組み合わせる際には、いくつかの特有の課題が発生します。これらの課題に対処するためには、適切な設計と実装が必要です。

例外の捕捉と伝播

非同期タスク内で発生した例外は、通常の同期処理とは異なり、即座に捕捉されません。非同期タスクの実行スレッドが異なるため、例外が発生した場所から呼び出し元に伝播する方法が異なります。

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

int riskyOperation() {
    throw std::runtime_error("非同期タスク内で例外が発生しました");
    return 42;
}

int main() {
    std::future<int> result = std::async(std::launch::async, riskyOperation);
    try {
        result.get();
    } catch (const std::runtime_error& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

スレッドの管理

非同期プログラミングでは、スレッドの管理が重要な課題となります。スレッドのライフサイクルを適切に管理しなければ、メモリリークやスレッドの競合が発生する可能性があります。

スレッドプールの使用

スレッドプールを使用することで、スレッドの作成と破棄のオーバーヘッドを減らし、効率的なスレッド管理が可能になります。

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

void threadFunction(int id) {
    if (id == 3) {
        throw std::runtime_error("スレッド内で例外が発生しました");
    }
    std::cout << "スレッド " << id << " が実行されています" << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    try {
        for (int i = 0; i < 5; ++i) {
            threads.emplace_back(threadFunction, i);
        }
        for (auto& t : threads) {
            t.join();
        }
    } catch (const std::runtime_error& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

例外の非同期処理

非同期タスクの例外を適切に処理するためには、例外を捕捉してからメインスレッドに伝播する仕組みが必要です。これにより、例外が発生した場合でもプログラム全体が適切に動作し続けることができます。

futureとpromiseを使用した例外処理

std::futurestd::promiseを組み合わせて使用することで、非同期タスク内で発生した例外をメインスレッドで捕捉することが可能です。

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

void taskWithException(std::promise<int>& prom) {
    try {
        throw std::runtime_error("非同期タスクで例外が発生");
        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(taskWithException, std::ref(prom));

    try {
        fut.get();
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }

    t.join();
    return 0;
}

非同期プログラミングと例外処理を組み合わせる際のこれらの課題を理解し、適切な手法で対処することで、より堅牢なプログラムを作成することができます。次のセクションでは、非同期プログラミングでの例外処理の具体的なパターンについて詳しく説明します。

非同期プログラミングでの例外処理のパターン

非同期プログラミングにおける例外処理は、複数の方法やパターンを駆使することで効果的に実装できます。ここでは、いくつかの一般的なパターンを紹介します。

未来オブジェクト(Future)を用いた例外処理

std::futureを使用することで、非同期タスクの結果を取得する際に例外を捕捉することができます。これにより、非同期タスク内で発生した例外を呼び出し元に伝播させることが可能です。

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

int asyncTask() {
    throw std::runtime_error("非同期タスク内で例外が発生");
    return 42;
}

int main() {
    std::future<int> result = std::async(std::launch::async, asyncTask);
    try {
        result.get();
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

プロミス(Promise)と未来オブジェクト(Future)を組み合わせる

std::promisestd::futureを組み合わせることで、非同期タスク内で発生した例外をより柔軟に扱うことができます。タスク内で例外が発生した場合、それをstd::promiseオブジェクトにセットし、呼び出し元でstd::futureオブジェクトを使って例外を捕捉します。

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

void taskWithException(std::promise<int>& prom) {
    try {
        throw std::runtime_error("非同期タスクで例外が発生");
        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(taskWithException, std::ref(prom));

    try {
        fut.get();
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }

    t.join();
    return 0;
}

継続タスク(Continuation Task)を用いた例外処理

継続タスクを使用すると、非同期タスクの終了後に別のタスクを実行できます。この方法は、非同期タスクの結果を利用して次の処理を行う際に便利です。C++17では、std::futureの代わりにstd::asyncthen関数を使って継続タスクを実装できます。

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

int asyncTask() {
    throw std::runtime_error("非同期タスク内で例外が発生");
    return 42;
}

int main() {
    std::future<int> result = std::async(std::launch::async, asyncTask);

    auto continuation = result.then([](std::future<int> fut) {
        try {
            int value = fut.get();
            std::cout << "結果: " << value << std::endl;
        } catch (const std::exception& e) {
            std::cerr << "エラー: " << e.what() << std::endl;
        }
    });

    continuation.wait();
    return 0;
}

タスクベースの例外処理

タスクベースの非同期プログラミングでは、各タスクが独立して実行され、各タスクの結果や例外を個別に処理することができます。これにより、タスク間の依存関係が少なくなり、エラーハンドリングが容易になります。

#include <iostream>
#include <future>
#include <vector>
#include <stdexcept>

void processTask(int id) {
    if (id == 2) {
        throw std::runtime_error("タスク内で例外が発生しました");
    }
    std::cout << "タスク " << id << " が完了しました" << std::endl;
}

int main() {
    std::vector<std::future<void>> tasks;
    for (int i = 0; i < 5; ++i) {
        tasks.push_back(std::async(std::launch::async, processTask, i));
    }

    for (auto& task : tasks) {
        try {
            task.get();
        } catch (const std::exception& e) {
            std::cerr << "エラー: " << e.what() << std::endl;
        }
    }

    return 0;
}

これらのパターンを活用することで、非同期プログラミングにおける例外処理を効果的に実装することができます。次のセクションでは、非同期プログラミングと例外処理を組み合わせた具体的なコード例を紹介します。

実践的なコード例

ここでは、非同期プログラミングと例外処理を組み合わせた具体的なコード例を紹介します。この例を通じて、理論的な概念を実際のプログラムにどのように適用するかを理解しましょう。

例:非同期タスクでのファイル読み込みと例外処理

この例では、非同期タスクを使用してファイルを読み込み、エラーが発生した場合に例外を処理する方法を示します。

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

// ファイルを非同期に読み込む関数
std::string readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        throw std::runtime_error("ファイルを開けませんでした: " + filename);
    }

    std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    return content;
}

// 非同期タスクを管理する関数
void processFileAsync(const std::string& filename) {
    std::future<std::string> result = std::async(std::launch::async, readFile, filename);

    try {
        std::string content = result.get();
        std::cout << "ファイルの内容:\n" << content << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
}

int main() {
    std::string filename = "example.txt";
    processFileAsync(filename);
    return 0;
}

例:複数の非同期タスクと例外処理

この例では、複数の非同期タスクを実行し、それぞれのタスクで発生した例外を処理する方法を示します。

#include <iostream>
#include <vector>
#include <future>
#include <stdexcept>

// 複数の非同期タスクを処理する関数
void performTasks() {
    auto task1 = std::async(std::launch::async, []() {
        if (true) { // 任意の条件で例外を発生させる
            throw std::runtime_error("タスク1で例外が発生しました");
        }
        return "タスク1の結果";
    });

    auto task2 = std::async(std::launch::async, []() {
        return "タスク2の結果";
    });

    std::vector<std::future<std::string>> tasks = {std::move(task1), std::move(task2)};

    for (auto& task : tasks) {
        try {
            std::string result = task.get();
            std::cout << "タスク結果: " << result << std::endl;
        } catch (const std::exception& e) {
            std::cerr << "エラー: " << e.what() << std::endl;
        }
    }
}

int main() {
    performTasks();
    return 0;
}

例:非同期タスクと継続タスクでの例外処理

この例では、非同期タスクとその後の継続タスクを組み合わせ、例外処理を行う方法を示します。

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

// 非同期タスク
std::string asyncTask() {
    throw std::runtime_error("非同期タスク内で例外が発生");
    return "タスクの結果";
}

// 継続タスク
void continuationTask(std::future<std::string> fut) {
    try {
        std::string result = fut.get();
        std::cout << "継続タスクの結果: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "継続タスク内でエラー: " << e.what() << std::endl;
    }
}

int main() {
    std::future<std::string> result = std::async(std::launch::async, asyncTask);
    auto continuation = result.then(continuationTask);
    continuation.wait();
    return 0;
}

これらの例を通じて、非同期プログラミングと例外処理の組み合わせがどのように機能するかを理解し、実際のアプリケーションに応用できるようになります。次のセクションでは、非同期プログラミングと例外処理を組み合わせる際のベストプラクティスについてまとめます。

ベストプラクティス

非同期プログラミングと例外処理を組み合わせる際には、いくつかのベストプラクティスを守ることで、コードの品質と信頼性を向上させることができます。ここでは、いくつかの重要なベストプラクティスを紹介します。

タスクの設計と分割

非同期タスクは、可能な限り小さく、独立して設計することが重要です。これにより、タスク間の依存関係を減らし、エラーハンドリングが容易になります。また、タスクを分割することで、並列処理の効果を最大化できます。

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

void performTask(int id) {
    std::cout << "タスク " << id << " を実行中" << std::endl;
}

int main() {
    std::vector<std::future<void>> tasks;
    for (int i = 0; i < 5; ++i) {
        tasks.emplace_back(std::async(std::launch::async, performTask, i));
    }

    for (auto& task : tasks) {
        task.get();
    }

    return 0;
}

例外の明示的な処理

非同期タスクで発生する例外は、必ず明示的に処理するようにします。これにより、エラーが見逃されることなく適切に対処できます。

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

int riskyOperation() {
    throw std::runtime_error("例外が発生しました");
    return 42;
}

int main() {
    std::future<int> result = std::async(std::launch::async, riskyOperation);
    try {
        result.get();
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

リソース管理の徹底

非同期プログラミングでは、リソースの管理が非常に重要です。RAII(Resource Acquisition Is Initialization)パターンを使用することで、リソースの確実な解放を保証します。

#include <iostream>
#include <fstream>
#include <future>
#include <stdexcept>

std::string readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        throw std::runtime_error("ファイルを開けませんでした");
    }
    std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    return content;
}

int main() {
    std::future<std::string> result = std::async(std::launch::async, readFile, "example.txt");
    try {
        std::string content = result.get();
        std::cout << "ファイルの内容:\n" << content << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "エラー: " << e.what() << std::endl;
    }
    return 0;
}

ログとデバッグの徹底

非同期タスクのデバッグは困難な場合があります。ログを適切に出力することで、問題の特定と解決が容易になります。タスクごとにログを分けると、トラブルシューティングがさらに効率的になります。

#include <iostream>
#include <future>
#include <vector>
#include <stdexcept>

void task(int id) {
    try {
        if (id == 2) {
            throw std::runtime_error("タスクで例外が発生");
        }
        std::cout << "タスク " << id << " 実行中" << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "タスク " << id << " エラー: " << e.what() << std::endl;
    }
}

int main() {
    std::vector<std::future<void>> tasks;
    for (int i = 0; i < 5; ++i) {
        tasks.emplace_back(std::async(std::launch::async, task, i));
    }

    for (auto& task : tasks) {
        task.get();
    }

    return 0;
}

スレッドプールの利用

スレッドプールを利用することで、スレッドの作成と破棄に伴うオーバーヘッドを削減し、効率的なスレッド管理が可能になります。

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

void performTask(int id) {
    if (id == 3) {
        throw std::runtime_error("タスクで例外が発生");
    }
    std::cout << "タスク " << id << " 実行中" << std::endl;
}

int main() {
    std::vector<std::future<void>> tasks;
    for (int i = 0; i < 5; ++i) {
        tasks.emplace_back(std::async(std::launch::async, performTask, i));
    }

    for (auto& task : tasks) {
        try {
            task.get();
        } catch (const std::exception& e) {
            std::cerr << "エラー: " << e.what() << std::endl;
        }
    }

    return 0;
}

これらのベストプラクティスを守ることで、非同期プログラミングと例外処理の複雑な課題に対処し、信頼性の高いソフトウェアを開発することができます。次のセクションでは、読者が理解を深めるための応用例と演習問題を提供します。

応用例と演習問題

ここでは、非同期プログラミングと例外処理の理解を深めるための応用例と演習問題を提供します。これらの例と問題を通じて、実践的なスキルを身につけましょう。

応用例1: 非同期ファイルダウンロードとエラーハンドリング

この例では、複数のファイルを非同期にダウンロードし、エラーが発生した場合に適切に処理する方法を示します。

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

// 模擬的なファイルダウンロード関数
void downloadFile(const std::string& url, const std::string& filename) {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // ダウンロードのシミュレーション
    if (url.empty()) {
        throw std::runtime_error("無効なURLです: " + url);
    }
    std::cout << "ダウンロード完了: " << filename << std::endl;
}

void downloadFiles(const std::vector<std::pair<std::string, std::string>>& files) {
    std::vector<std::future<void>> tasks;
    for (const auto& file : files) {
        tasks.emplace_back(std::async(std::launch::async, downloadFile, file.first, file.second));
    }

    for (auto& task : tasks) {
        try {
            task.get();
        } catch (const std::exception& e) {
            std::cerr << "エラー: " << e.what() << std::endl;
        }
    }
}

int main() {
    std::vector<std::pair<std::string, std::string>> files = {
        {"http://example.com/file1", "file1.txt"},
        {"http://example.com/file2", "file2.txt"},
        {"", "file3.txt"} // エラーを発生させるための無効なURL
    };

    downloadFiles(files);
    return 0;
}

応用例2: 非同期データ処理と例外の伝播

この例では、データ処理タスクを非同期に実行し、発生した例外をメインスレッドに伝播させる方法を示します。

#include <iostream>
#include <future>
#include <vector>
#include <stdexcept>

// データ処理関数
int processData(int data) {
    if (data < 0) {
        throw std::runtime_error("負のデータは処理できません: " + std::to_string(data));
    }
    return data * 2;
}

void processDataset(const std::vector<int>& dataset) {
    std::vector<std::future<int>> tasks;
    for (int data : dataset) {
        tasks.emplace_back(std::async(std::launch::async, processData, data));
    }

    for (auto& task : tasks) {
        try {
            int result = task.get();
            std::cout << "処理結果: " << result << std::endl;
        } catch (const std::exception& e) {
            std::cerr << "エラー: " << e.what() << std::endl;
        }
    }
}

int main() {
    std::vector<int> dataset = {1, 2, -1, 4, 5}; // 負のデータを含む
    processDataset(dataset);
    return 0;
}

演習問題

  1. 演習問題1: タスクのタイムアウト処理
  • 非同期タスクが一定時間内に完了しない場合に、タイムアウト例外を発生させる機能を実装してください。
  1. 演習問題2: 非同期タスクのリトライ処理
  • 非同期タスクが失敗した場合に、一定回数リトライする機能を実装してください。各リトライの間には遅延を挿入してください。
  1. 演習問題3: 非同期タスクの結果を集約
  • 複数の非同期タスクの結果を集約し、一つの結果として返す関数を実装してください。すべてのタスクが成功するまで待機し、どれか一つが失敗した場合は適切に例外を処理してください。

これらの応用例と演習問題に取り組むことで、非同期プログラミングと例外処理に対する理解を深め、実践的なスキルを身につけることができます。次のセクションでは、本記事のまとめを行います。

まとめ

本記事では、C++における非同期プログラミングと例外処理の基礎から応用までを詳しく解説しました。非同期プログラミングの基本概念や利点、C++での具体的な手法、そして例外処理の基本と実装方法について学びました。さらに、非同期プログラミングと例外処理を組み合わせる際の課題や、それを解決するためのパターン、実践的なコード例を提供しました。

これらの知識を基に、堅牢で効率的な非同期プログラムを作成するためのスキルを習得し、実際のプロジェクトで応用することができるでしょう。今後の開発において、非同期プログラミングと例外処理のベストプラクティスを活用し、高品質なソフトウェアを開発することを目指してください。

コメント

コメントする

目次
  1. 非同期プログラミングの基礎
    1. パフォーマンスの向上
    2. ユーザー体験の向上
    3. リソースの効率的な利用
  2. C++における非同期プログラミングの手法
    1. std::async
    2. std::thread
    3. Boost.Asio
  3. 例外処理の基礎
    1. 例外の発生と捕捉
    2. 標準例外クラス
    3. カスタム例外クラス
  4. C++における例外処理の実装
    1. 基本的な例外処理の構造
    2. 複数の例外を捕捉する
    3. 標準ライブラリの例外クラス
    4. カスタム例外クラスの作成
    5. リソース管理とRAII
  5. 非同期プログラミングと例外処理の組み合わせの課題
    1. 例外の捕捉と伝播
    2. スレッドの管理
    3. 例外の非同期処理
  6. 非同期プログラミングでの例外処理のパターン
    1. 未来オブジェクト(Future)を用いた例外処理
    2. プロミス(Promise)と未来オブジェクト(Future)を組み合わせる
    3. 継続タスク(Continuation Task)を用いた例外処理
    4. タスクベースの例外処理
  7. 実践的なコード例
    1. 例:非同期タスクでのファイル読み込みと例外処理
    2. 例:複数の非同期タスクと例外処理
    3. 例:非同期タスクと継続タスクでの例外処理
  8. ベストプラクティス
    1. タスクの設計と分割
    2. 例外の明示的な処理
    3. リソース管理の徹底
    4. ログとデバッグの徹底
    5. スレッドプールの利用
  9. 応用例と演習問題
    1. 応用例1: 非同期ファイルダウンロードとエラーハンドリング
    2. 応用例2: 非同期データ処理と例外の伝播
    3. 演習問題
  10. まとめ