アプリケーションのボタンをクリックしたとき、重い処理が始まって画面全体がフリーズ(応答なし)になってしまった…そんな経験はありませんか?これは、時間のかかる処理(同期処理)がメインの処理をブロックしてしまうために起こります。
この記事では、このような問題を解決し、アプリケーションの応答性を高く保つための強力な仕組み、「非同期処理」を簡単に実現する方法を紹介します。
目次
非同期処理の考え方:カフェのブザーに例えてみよう ☕
非同期処理を理解するために、カフェでの注文をイメージしてみましょう。
- 同期処理: レジで注文した後、その場でコーヒーが出来上がるまでずっと待ち続ける。他のことは何もできません。
- 非同期処理: レジで注文すると、番号の書かれたブザーを渡されます。あなたは席についてスマホを見たり、他の用事を済ませたりできます。ブザーが鳴ったら、コーヒーを受け取りに行きます。
この「ブザーを渡されて、待ち時間を自由にする」仕組みが非同期処理の基本です。標準ライブラリでは、これをstd::async
とstd::future
という2つの機能で実現します。
std::async
: 重い処理の実行をバックグラウンドで依頼し、future
(ブザー)を受け取る。std::future
: いずれ結果が入る「未来の入れ物」。結果(コーヒー)の準備ができると、get()
メソッドで取り出せる。
async
とfuture
の基本的な使い方
それでは、実際にコードを見てみましょう。ここでは、データベースから重いデータを集計する、という時間のかかる処理をシミュレートします。
#include <iostream>
#include <future> // async, future のために必要
#include <vector>
#include <numeric> // accumulate のために必要
#include <chrono> // 時間を扱うために必要
#include <thread> // スレッドを扱うために必要
// 時間のかかる重い処理をシミュレートする関数
long long aggregate_data(const std::vector<int>& data_set) {
std::cout << " (バックグラウンド処理開始...)\n";
// 2秒かかる重い処理をシミュレート
std::this_thread::sleep_for(std::chrono::seconds{2});
long long sum = std::accumulate(data_set.begin(), data_set.end(), 0LL);
std::cout << " (バックグラウンド処理完了...)\n";
return sum;
}
int main() {
std::vector<int> sample_data = {10, 20, 30, 40, 50};
// 1. 非同期タスクの起動(バックグラウンドでの処理を依頼し、未来の結果を受け取る)
// launch::async は、新しいスレッドで実行することを保証する起動ポリシー
std::future<long long> result_future =
std::async(std::launch::async, aggregate_data, std::ref(sample_data));
// 2. メインスレッドは別の作業を続けられる
std::cout << "メイン処理: バックグラウンドで集計を開始しました。他の作業を進めます。\n";
// ... ここでUIの更新など、他の軽い処理を行う ...
std::cout << "メイン処理: 結果が必要になるまで、ここで待機します。\n";
// 3. 結果が必要になったら、get() で待機して取得する
// バックグラウンド処理が終わっていなければ、ここで完了まで待機(ブロック)する
long long final_result = result_future.get();
std::cout << "メイン処理: 結果を取得しました! 集計結果: " << final_result << std::endl;
return 0;
}
実行結果
メイン処理: バックグラウンドで集計を開始しました。他の作業を進めます。
(バックグラウンド処理開始...)
メイン処理: 結果が必要になるまで、ここで待機します。
(バックグラウンド処理完了...)
メイン処理: 結果を取得しました! 集計結果: 150
メインの処理がバックグラウンド処理の完了を待たずに進んでいることが分かります。
コードのポイント解説
std::async(std::launch::async, ...)
第一引数のstd::launch::async
は、「このタスクを新しいスレッドですぐに実行してください」と明示的に指示する起動ポリシーです。std::ref(sample_data)
async
は引数をコピーして渡すのが基本です。sample_data
のような大きなデータをコピーするのは非効率なため、std::ref
を使って参照渡しにしています。std::future<long long>
aggregate_data
関数の戻り値の型(long long
)に合わせて、future
のテンプレート引数を指定します。result_future.get()
future
から結果を取得するための関数です。もしバックグラウンド処理がまだ完了していなければ、ここで完了するまで処理を停止(ブロック)します。また、get()
は一度しか呼び出せません。- 例外処理 もし
aggregate_data
関数内で例外が発生した場合、その例外はfuture
オブジェクトに保存されます。そして、get()
を呼び出した瞬間に、その例外がメインスレッド側で再送出されます。そのため、get()
はtry-catch
ブロックで囲むのが安全です。
まとめ
std::async
とstd::future
は、専門的なスレッド管理の知識がなくても、手軽に非同期処理を導入できる非常に便利なツールです。
時間のかかる処理をバックグラウンドに任せることで、ユーザーインターフェースの応答性を保ち、ユーザー体験を大きく向上させることができます。アプリケーションが「固まる」問題に悩んでいるなら、ぜひこの強力な機能の導入を検討してみてください。