Javaにおける非同期メソッド呼び出しパターン:非同期プログラミングによるパフォーマンス向上
別名
- 非同期プロシージャコール
非同期メソッド呼び出しデザインパターンの目的
非同期メソッド呼び出しパターンは、メソッドを非同期的に呼び出すことを可能にすることで、並行性を高めるように設計されています。このパターンは、並列タスクの実行、待ち時間の短縮、システムスループットの向上に役立ちます。
実際の例を用いた非同期メソッド呼び出しパターンの詳細な説明
実際の例
非同期メソッド呼び出しは、ノンブロッキング操作を可能にし、複数のプロセスを同時に実行できるようにします。このパターンは、Webサーバーやマイクロサービスなど、高いスケーラビリティとパフォーマンスを必要とするアプリケーションで特に役立ちます。
宇宙ロケットの文脈では、非同期メソッド呼び出しパターンの類似例は、ミッションコントロールセンターとロケットの搭載システム間の通信に見られます。ミッションコントロールがロケットに軌道調整やシステムチェックを実行するコマンドを送信する場合、ロケットがタスクを完了して報告するまでアイドル状態で待つことはありません。代わりに、ミッションコントロールはミッションの他の側面を監視し、さまざまなタスクを管理し続けます。ロケットはコマンドを非同期的に実行し、操作が完了したら、ステータス更新または結果をミッションコントロールに送り返します。これにより、ミッションコントロールは、ソフトウェアシステムにおける非同期メソッド呼び出しの動作と同様に、単一のタスクによってブロックされることなく、複数の同時操作を効率的に管理できます。
簡単な言葉で
非同期メソッド呼び出しは、タスクの処理を開始し、タスクの準備ができる前にすぐに制御を返します。タスク処理の結果は、後で呼び出し元に返されます。
Wikipediaによると
マルチスレッドコンピュータプログラミングでは、非同期メソッド呼び出し(AMI)、非同期メソッドコール、または非同期パターンとも呼ばれ、呼び出し側が呼び出されたコードの終了を待機している間ブロックされない設計パターンです。代わりに、応答が到着したときに呼び出しスレッドに通知されます。応答のポーリングは望ましくないオプションです。
Javaにおける非同期メソッド呼び出しパターンのプログラム例
複数のタスクを同時に実行する必要があるシナリオを考えてみましょう。非同期メソッド呼び出しパターンを使用すると、各タスクの完了を待たずにこれらのタスクを開始できるため、リソースの使用が最適化され、レイテンシが短縮されます。
この例では、宇宙ロケットの打ち上げと月面探査機の展開を行っています。
このアプリケーションは、非同期メソッド呼び出しパターンを示しています。パターンの主要部分は、非同期的に評価された値のための中間コンテナである`AsyncResult`、タスク完了時に実行されるように提供できる`AsyncCallback`、および非同期タスクの実行を管理する`AsyncExecutor`です。
public interface AsyncResult<T> {
boolean isCompleted();
T getValue() throws ExecutionException;
void await() throws InterruptedException;
}
public interface AsyncCallback<T> {
void onComplete(T value);
void onError(Exception ex);
}
public interface AsyncExecutor {
<T> AsyncResult<T> startProcess(Callable<T> task);
<T> AsyncResult<T> startProcess(Callable<T> task, AsyncCallback<T> callback);
<T> T endProcess(AsyncResult<T> asyncResult) throws ExecutionException, InterruptedException;
}
`ThreadAsyncExecutor`は、`AsyncExecutor`の実装です。その主要部分のいくつかを次に示します。
public class ThreadAsyncExecutor implements AsyncExecutor {
@Override
public <T> AsyncResult<T> startProcess(Callable<T> task) {
return startProcess(task, null);
}
@Override
public <T> AsyncResult<T> startProcess(Callable<T> task, AsyncCallback<T> callback) {
var result = new CompletableResult<>(callback);
new Thread(
() -> {
try {
result.setValue(task.call());
} catch (Exception ex) {
result.setException(ex);
}
},
"executor-" + idx.incrementAndGet())
.start();
return result;
}
@Override
public <T> T endProcess(AsyncResult<T> asyncResult)
throws ExecutionException, InterruptedException {
if (!asyncResult.isCompleted()) {
asyncResult.await();
}
return asyncResult.getValue();
}
}
これで、ロケットを打ち上げて、すべてがどのように連携して動作するかを確認する準備が整いました。
public static void main(String[] args) throws Exception {
// construct a new executor that will run async tasks
var executor = new ThreadAsyncExecutor();
// start few async tasks with varying processing times, two last with callback handlers
final var asyncResult1 = executor.startProcess(lazyval(10, 500));
final var asyncResult2 = executor.startProcess(lazyval("test", 300));
final var asyncResult3 = executor.startProcess(lazyval(50L, 700));
final var asyncResult4 = executor.startProcess(lazyval(20, 400),
callback("Deploying lunar rover"));
final var asyncResult5 =
executor.startProcess(lazyval("callback", 600), callback("Deploying lunar rover"));
// emulate processing in the current thread while async tasks are running in their own threads
Thread.sleep(350); // Oh boy, we are working hard here
log("Mission command is sipping coffee");
// wait for completion of the tasks
final var result1 = executor.endProcess(asyncResult1);
final var result2 = executor.endProcess(asyncResult2);
final var result3 = executor.endProcess(asyncResult3);
asyncResult4.await();
asyncResult5.await();
// log the results of the tasks, callbacks log immediately when complete
log(String.format(ROCKET_LAUNCH_LOG_PATTERN, result1));
log(String.format(ROCKET_LAUNCH_LOG_PATTERN, result2));
log(String.format(ROCKET_LAUNCH_LOG_PATTERN, result3));
}
プログラムのコンソール出力は次のとおりです。
21:47:08.227[executor-2]INFO com.iluwatar.async.method.invocation.App-Space rocket<test> launched successfully
21:47:08.269[main]INFO com.iluwatar.async.method.invocation.App-Mission command is sipping coffee
21:47:08.318[executor-4]INFO com.iluwatar.async.method.invocation.App-Space rocket<20>launched successfully
21:47:08.335[executor-4]INFO com.iluwatar.async.method.invocation.App-Deploying lunar rover<20>
21:47:08.414[executor-1]INFO com.iluwatar.async.method.invocation.App-Space rocket<10>launched successfully
21:47:08.519[executor-5]INFO com.iluwatar.async.method.invocation.App-Space rocket<callback> launched successfully
21:47:08.519[executor-5]INFO com.iluwatar.async.method.invocation.App-Deploying lunar rover<callback>
21:47:08.616[executor-3]INFO com.iluwatar.async.method.invocation.App-Space rocket<50>launched successfully
21:47:08.617[main]INFO com.iluwatar.async.method.invocation.App-Space rocket<10>launch complete
21:47:08.617[main]INFO com.iluwatar.async.method.invocation.App-Space rocket<test> launch complete
21:47:08.618[main]INFO com.iluwatar.async.method.invocation.App-Space rocket<50>launch complete
Javaで非同期メソッド呼び出しパターンを使用する場合
このパターンは、複数の並列タスクを効率的に管理する必要があるアプリケーションに最適です。バックグラウンドプロセスの処理、ユーザーインターフェイスの応答性の向上、非同期データ処理の管理などのシナリオでよく使用されます。
以下の場合に非同期メソッド呼び出しパターンを使用します
- プログラムの次の手順に進む前に操作を完了する必要がない場合。
- IO操作、ネットワークリクエスト、複雑な計算など、リソースを大量に消費したり、時間がかかったりするタスクの場合、操作を同期的に行うとパフォーマンスやユーザーエクスペリエンスに大きな影響が出ます。
- GUIアプリケーションでは、長時間実行されるタスク中にフリーズや応答しなくなるのを防ぐため。
- Webアプリケーションでは、ノンブロッキングIO操作のため。
Javaにおける非同期メソッド呼び出しパターンの実際のアプリケーション
多くの最新のアプリケーションは、同時リクエストを処理するWebサーバー、マイクロサービスアーキテクチャ、集中的なバックグラウンド処理を必要とするシステムなど、非同期メソッド呼び出しパターンを活用しています。
- スループットを向上させ、レイテンシを 줄이기 위해 HTTPリクエストを非同期的に処理するWebサーバー。
- ユーザーインターフェイスをブロックすることなく、時間のかかる操作を実行するためにバックグラウンドスレッドを使用するデスクトップおよびモバイルアプリケーション。
- メッセージキューまたはイベントストリームを介してサービスが非同期通信を実行するマイクロサービスアーキテクチャ。
- FutureTask
- CompletableFuture
- ExecutorService
- タスクベースの非同期パターン
非同期メソッド呼び出しパターンの利点とトレードオフ
このパターンは大きなパフォーマンス上の利点をもたらしますが、エラー処理とリソース管理の複雑さも増します。競合状態やデッドロックなどの潜在的な落とし穴を回避するには、適切な実装が不可欠です。
利点
- 応答性の向上:メインスレッドまたはアプリケーションフローはブロックされないままであるため、GUIアプリケーションのユーザーエクスペリエンスと全体的な応答性が向上します。
- リソース使用率の向上:並列実行を可能にすることにより、システムリソース(CPUやIOなど)がより効率的に利用され、アプリケーションのスループットが向上する可能性があります。
- スケーラビリティ:タスクをより効果的に利用可能なリソースに分散できるため、アプリケーションのスケーリングが容易になります。
トレードオフ
- 複雑さ:非同期操作を導入すると、コードベースが複雑になり、理解、デバッグ、保守がより困難になる可能性があります。
- リソース管理:スレッドまたは実行コンテキストを慎重に管理する必要があります。これにより、オーバーヘッドと潜在的なリソース枯渇の問題が発生する可能性があります。
- エラー処理:非同期操作により、異なるスレッドまたは異なる時間に例外が発生する可能性があるため、エラー処理がより複雑になる可能性があります。
関連するJavaデザインパターン
非同期メソッド呼び出しパターンは、リクエストをカプセル化するためのコマンドパターン、イベント処理のためのオブザーバーパターン、非同期結果を管理するためのPromiseパターンなど、他のデザインパターンと連携してうまく機能することがよくあります。
- コマンド:非同期メソッド呼び出しを使用して、コマンドが非同期的に実行されるコマンドパターンを実装できます。
- オブザーバー:非同期メソッド呼び出しを使用して、サブジェクトの状態が変化したときにオブザーバーに非同期的に通知できます。
- Promise:AsyncResultインターフェースは、まだ利用できない可能性のある値を表すPromiseの形式と見なすことができます。