読み込み中...

【C++】非同期処理を完全攻略するための10のサンプルコード

C++における非同期処理を完全攻略するための徹底解説のイメージ C++
この記事は約18分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

C++を学ぶ上で欠かせないのが「非同期処理」です。

この記事では、C++における非同期処理を完全に理解し、実践的なスキルを身につけることを目指します。

初心者から上級者まで、幅広い読者層に対応するため、基本的な概念から応用例まで、豊富なサンプルコードを交えて解説していきます。

この記事を読み終えた時、あなたはC++での非同期処理を自在に操れるようになるでしょう。

●C++非同期処理とは

C++における非同期処理とは、メインのプログラムの実行を妨げることなく、バックグラウンドで別のタスクを実行する技術です。

これにより、プログラムは複数の作業を同時に行うことができ、より効率的かつ高速な実行が可能になります。

特に、リアルタイム性が求められるアプリケーションや、大量のデータを扱う場合に、非同期処理は重要な役割を果たします。

○非同期処理の基本概念

非同期処理の基本は、メインの処理とは独立して別の処理を行うことです。

これにより、例えば大量のデータの読み込みや、時間がかかる計算処理を行っている間にも、ユーザーインターフェースはスムーズに動作し続けることができます。

非同期処理を実現するためには、スレッドやタスクといった概念が用いられます。

これらを適切に管理し、効率的にプログラムを設計することが重要です。

○C++における非同期処理の重要性

C++で非同期処理を用いることで、アプリケーションのパフォーマンスを大幅に向上させることが可能です。

特に、マルチコアプロセッサが普及した現代においては、非同期処理を通じて複数のコアを効率良く活用することが重要です。

また、非同期処理を適切に実装することで、プログラムの応答性を高め、ユーザーエクスペリエンスを向上させることができます。

C++の強力な機能を活かした非同期処理は、高度なプログラミング技術の一つとして、あらゆるC++開発者にとって必須のスキルと言えるでしょう。

●非同期処理の基本的な使い方

C++における非同期処理は、プログラムが複数のタスクを同時に実行できるようにする強力な機能です。

非同期処理を用いることで、アプリケーションのレスポンス性を向上させたり、リソースの利用効率を高めたりすることができます。

ここでは、C++での非同期処理の基本的な使い方を、詳細な説明とサンプルコードを交えて解説します。

○サンプルコード1:基本的な非同期タスクの作成

非同期処理を行う基本的な方法の一つとして、std::async 関数を使用する方法があります。

この関数は、新しいスレッドで関数またはタスクを実行し、その実行結果をstd::futureオブジェクトとして返します。

下記のサンプルコードでは、簡単な計算タスクを非同期で実行しています。

#include <future>
#include <iostream>

int calculate() {
    // 何らかの計算を行う
    return 42;
}

int main() {
    // 非同期タスクの開始
    std::future<int> result = std::async(calculate);

    // ここで他の処理を行うことができる

    // タスクの結果を取得
    std::cout << "Result: " << result.get() << std::endl;
    return 0;
}

この例では、calculate関数を非同期で実行し、その結果をstd::futureオブジェクトで受け取ります。

result.get()を呼び出すことで、タスクの結果を取得し、コンソールに出力しています。

○サンプルコード2:非同期タスクの終了を待つ

非同期タスクの結果を待つ際には、std::futureオブジェクトのwaitメソッドやgetメソッドを使用します。

waitメソッドはタスクの終了を待ちますが、結果を取得しません。

一方、getメソッドはタスクの結果を取得します。

下記のサンプルコードは、非同期タスクの終了を待つ方法を表しています。

#include <future>
#include <iostream>
#include <chrono>

void longTask() {
    // 長い処理を模倣
    std::this_thread::sleep_for(std::chrono::seconds(3));
}

int main() {
    // 非同期タスクの開始
    std::future<void> future = std::async(longTask);

    // タスクの終了を待つ
    future.wait();
    std::cout << "Task completed" << std::endl;

    return 0;
}

このコードでは、longTask関数を非同期で実行し、future.wait()を使用してタスクの終了を待ちます。

タスクが完了すると、”Task completed”と出力されます。

●非同期処理の応用

C++での非同期処理は、基本的な使い方だけでなく、より複雑なシナリオにも対応できます。

ここでは、複数の非同期タスクを管理する方法や、非同期タスクのキャンセルなどの応用的な使い方を紹介します。

○サンプルコード3:複数の非同期タスクの管理

非同期タスクを複数管理する際には、std::vectorstd::arrayを使用して、複数のstd::futureオブジェクトを格納できます。

下記のサンプルコードでは、3つの非同期タスクを同時に実行し、それぞれの結果を待機する方法を表しています。

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

int task(int number) {
    // 何らかの処理を行う
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return number * 2;
}

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

    // 3つの非同期タスクを開始
    for (int i = 0; i < 3; ++i) {
        futures.push_back(std::async(task, i));
    }

    // 各タスクの結果を取得
    for (auto& future : futures) {
        std::cout << "Result: " << future.get() << std::endl;
    }

    return 0;
}

このコードでは、3つの非同期タスクがtask関数を通じて開始され、それぞれの結果がfuturesベクタに保存されます。

ループを使用して、それぞれのタスクの結果を取得しています。

○サンプルコード4:非同期タスクのキャンセル

C++標準ライブラリでは、非同期タスクを直接キャンセルする機能は提供されていません。

しかし、タスク内部で条件をチェックし、外部からの信号によってタスクを終了させることは可能です。

下記のサンプルコードは、外部からのフラグをチェックしてタスクを終了させる方法を表しています。

#include <future>
#include <iostream>
#include <atomic>
#include <chrono>

std::atomic<bool> cancelFlag(false);

void longRunningTask() {
    int counter = 0;
    while (!cancelFlag) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "Running... " << counter++ << std::endl;
    }
    std::cout << "Task cancelled" << std::endl;
}

int main() {
    // 非同期タスクの開始
    std::future<void> future = std::async(longRunningTask);

    // 5秒後にタスクをキャンセル
    std::this_thread::sleep_for(std::chrono::seconds(5));
    cancelFlag = true;

    // タスクの終了を待つ
    future.wait();

    return 0;
}

この例では、cancelFlagというグローバルなstd::atomic<bool>変数を使用して、タスクがキャンセルされたかどうかをチェックしています。

メインスレッドでは、5秒後にこのフラグをtrueに設定し、非同期タスクを終了させています。

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

C++の非同期処理では、エラーハンドリングと例外処理が重要な役割を果たします。

非同期タスクの実行中に発生する可能性のあるエラーや例外を適切に処理することで、安定性と信頼性の高いプログラムを作成できます。

ここでは、非同期処理におけるエラーハンドリングと例外処理の方法を、具体的なサンプルコードとともに解説します。

○サンプルコード5:非同期タスクでの例外処理

非同期タスクで例外が発生した場合、その例外はstd::futureオブジェクトを通じて伝播されます。

下記のサンプルコードは、非同期タスクで例外を投げ、メインスレッドでその例外をキャッチする方法を表しています。

#include <future>
#include <iostream>

void taskThatThrows() {
    throw std::runtime_error("Error in task");
}

int main() {
    // 非同期タスクの開始
    std::future<void> future = std::async(taskThatThrows);

    try {
        // タスクの終了を待ち、例外をキャッチする
        future.get();
    } catch(const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}

このコードでは、taskThatThrows関数内で例外が発生し、future.get()を呼び出すことでメインスレッドに伝播されます。

メインスレッドではtry-catchブロックを使用して例外をキャッチし、エラーメッセージを表示しています。

○サンプルコード6:エラーハンドリングの実装

非同期処理におけるエラーハンドリングは、プログラムの安定性を保つために不可欠です。

下記のサンプルコードでは、非同期タスクが失敗した場合に適切なエラーハンドリングを行う方法を表しています。

#include <future>
#include <iostream>

int taskWithPotentialError() {
    // 50%の確率でエラーを発生させる
    if (rand() % 2 == 0) {
        throw std::runtime_error("Random error occurred");
    }
    return 42;
}

int main() {
    // 非同期タスクの開始
    std::future<int> future = std::async(taskWithPotentialError);

    try {
        // タスクの結果を取得
        int result = future.get();
        std::cout << "Result: " << result << std::endl;
    } catch(const std::exception& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }

    return 0;
}

この例では、非同期タスクtaskWithPotentialErrorがランダムにエラーを発生させる可能性があります。

future.get()を呼び出すことで、タスクの成功または失敗に対応する結果または例外を取得し、適切に処理を行っています。

●高度な非同期処理テクニック

C++における高度な非同期処理テクニックは、より複雑なアプリケーションやシステムにおいて重要な役割を果たします。

これらのテクニックを活用することで、効率的なマルチスレッド処理やイベント駆動型のプログラミングが可能になります。

ここでは、マルチスレッドでの非同期処理とイベント駆動型の非同期処理について、具体的なサンプルコードと共に解説します。

○サンプルコード7:マルチスレッドでの非同期処理

C++11以降では、std::threadライブラリを使用してマルチスレッドのプログラミングが容易になりました。

下記のサンプルコードは、複数のスレッドを使用して非同期タスクを並行して実行する方法を表しています。

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

void task(int num) {
    // 何らかのタスクを実行
    std::cout << "Task " << num << " is running" << std::endl;
}

int main() {
    std::vector<std::thread> threads;

    // 複数のスレッドを生成してタスクを実行
    for (int i = 0; i < 5; ++i) {
        threads.push_back(std::thread(task, i));
    }

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

    return 0;
}

このコードでは、5つの異なるスレッドがtask関数を並行して実行します。

各スレッドは別々のタスク番号を持ち、それぞれ独立して処理を行います。

joinメソッドを使用することで、メインスレッドはすべてのスレッドが終了するまで待機します。

○サンプルコード8:イベント駆動型の非同期処理

イベント駆動型のプログラミングでは、特定のイベントが発生したときにタスクを実行します。

これにより、ユーザーのアクションやシステムの状態変化に応じた動的な処理が可能になります。

下記のサンプルコードは、イベント駆動型の非同期処理の基本的な構造を表しています。

#include <iostream>
#include <functional>
#include <map>
#include <string>

class EventManager {
public:
    void subscribe(const std::string& event, std::function<void()> handler) {
        handlers[event].push_back(handler);
    }

    void trigger(const std::string& event) {
        for (const auto& handler : handlers[event]) {
            handler();
        }
    }

private:
    std::map<std::string, std::vector<std::function<void()>>> handlers;
};

int main() {
    EventManager manager;

    // イベントハンドラの登録
    manager.subscribe("start", []() { std::cout << "Start event triggered" << std::endl; });
    manager.subscribe("stop", []() { std::cout << "Stop event triggered" << std::endl; });

    // イベントのトリガー
    manager.trigger("start");
    manager.trigger("stop");

    return 0;
}

このコードでは、EventManagerクラスを使用してイベントとそれに対応するハンドラ(処理)を管理しています。

イベントがトリガーされると、そのイベントに登録されたすべてのハンドラが実行されます。

このようにイベント駆動型のアプローチを取ることで、柔軟かつ効率的な非同期処理が実現できます。

●パフォーマンスと最適化

C++における非同期処理のパフォーマンスと最適化は、高速で効率的なアプリケーション開発に不可欠です。

リソースの管理とメモリの最適化は、システムの負荷を軽減し、よりスムーズな実行を可能にします。

ここでは、非同期処理のパフォーマンスを最適化する方法と、リソース管理及びメモリ最適化のテクニックを、具体的なサンプルコードを交えて紹介します。

○サンプルコード9:非同期処理のパフォーマンス最適化

非同期処理のパフォーマンスを最適化するためには、タスクの分割と負荷の均等化が鍵となります。

下記のサンプルコードは、複数のタスクを均等に分割し、それぞれを非同期に実行する方法を表しています。

#include <future>
#include <vector>

int performTask(int part) {
    // タスクの実行
    return part * part;
}

int main() {
    const int numTasks = 10;
    std::vector<std::future<int>> futures;

    // 複数のタスクを非同期で実行
    for (int i = 0; i < numTasks; ++i) {
        futures.push_back(std::async(performTask, i));
    }

    // 各タスクの結果を集計
    int total = 0;
    for (auto& future : futures) {
        total += future.get();
    }

    return 0;
}

この例では、performTask関数を非同期に実行し、複数のタスクを並行して処理しています。

これにより、全体の処理時間を短縮し、パフォーマンスを最適化することができます。

○サンプルコード10:リソース管理とメモリ最適化

リソース管理とメモリ最適化は、非同期処理のパフォーマンス向上に直結します。

下記のサンプルコードは、メモリリソースを効率的に使用するためのテクニックを表しています。

#include <future>
#include <memory>

class Resource {
public:
    Resource() {
        // リソースの初期化
    }

    void use() {
        // リソースの使用
    }
};

int main() {
    // スマートポインタを使用してリソースの管理を行う
    auto resource = std::make_shared<Resource>();

    auto task = [resource]() {
        resource->use();
    };

    // 非同期タスクの実行
    std::future<void> future = std::async(task);

    return 0;
}

このコードでは、スマートポインタ(std::shared_ptr)を使用してリソースのライフサイクルを管理しています。

これにより、メモリリークのリスクを減らし、リソースの効率的な使用が可能になります。

●注意点とよくある間違い

C++における非同期処理は非常に強力ですが、適切な使用方法を理解し、一般的な間違いを避けることが重要です。

非同期処理における注意点と、よくある間違いとその対処法について詳細に解説します。

○非同期処理の際の注意点

非同期処理を使用する際には、いくつかの重要な点に注意する必要があります。

まず、非同期タスクがメインスレッドと同時にアクセスする可能性のある共有リソースには特に注意が必要です。

これには、データの競合やデッドロックの防止が含まれます。

また、非同期タスクのライフサイクル管理にも注意が必要で、タスクが終了する前にプログラムが終了しないようにする必要があります。

○よくある間違いとその対処法

非同期処理におけるよくある間違いの一つに、スレッドセーフでないリソースへの同時アクセスがあります。

この問題を解決するためには、ミューテックスやロックを使用してアクセスを制御する必要があります。

また、非同期タスクが長時間実行される場合、メインスレッドがタスクの終了を待たなければならない場合があります。

これを防ぐためには、std::futurestd::asyncを適切に使用し、タスクの完了を適切にハンドルすることが重要です。

さらに、非同期タスク内で例外が発生した場合、それを適切にキャッチし処理することが重要です。

非同期タスクの例外は、std::futureを使用してメインスレッドに伝達されるため、future.get()を呼び出す際に例外をキャッチし適切に処理することが求められます。

まとめ

この記事では、C++における非同期処理の基本から高度なテクニックまでを、具体的なサンプルコードと共に解説しました。

非同期処理は、効率的なプログラミングに不可欠ですが、その実装には注意が必要です。

エラーハンドリング、リソース管理、パフォーマンスの最適化など、さまざまな側面から非同期処理を理解し、適切に使用することが重要です。

この知識を活用して、より高品質で効率的なC++アプリケーションの開発に取り組んでいただければと思います。