Java非同期処理
Spring @Asyncを見る前に、同期、非同期、マルチスレッドの概念は必須です。該当の概念を知っていることを前提とし、純粋なJava非同期処理方式をコードで見てみましょう。もしJavaスレッドに慣れているなら、該当の章はスキップしても構いません。
もしmessageを受け取って単に出力する機能を同期方式で作るなら、上記のようにコードを記述できます。これをマルチスレッディング非同期方式に変えると、以下のようにソースコードを記述できます。
しかし、この方法はThreadを管理できないため非常に危険です。例えば、同時に10,000個の呼び出しが行われると、非常に短い時間でThreadを10,000個生成する必要があります。Threadを生成するコストは少なくないため、プログラムのパフォーマンスに悪影響を及ぼし、場合によってはOOMエラーが発生する可能性があります。そのため、Threadを管理するにはThread Poolを実装する必要があり、JavaではExecutorServiceクラスを提供しています。
全スレッドの個数を10個に制限しており、私たちが望むマルチスレッディング方式の非同期処理も正しく行えるようになりました。しかし、上記方式は非同期方式で処理したいメソッドごとにExecutorServiceのsubmit()メソッドを適用する必要があるため、繰り返し修正作業を行う必要があります。つまり、最初は同期ロジックで作成したメソッドを非同期に変えたい場合、メソッド自体のロジック変更が避けられないということです。
Spring @Async
単純な方法
@EnableAsyncアノテーションをApplicationクラスの上に貼り付け、非同期方式で処理したい同期ロジックのメソッドの上に@Asyncアノテーションを貼れば終わりです。しかし、上記方式はスレッドを管理しないという問題があります。なぜなら、@Asyncのデフォルト設定はSimpleAsyncTaskExecutorを使用するように設定されているためですが、これはスレッドプールではなく、単にスレッドを作成する役割をするからです。
スレッドプールを使用する方法
まず、Applicationクラスから@EnableAsyncを削除します。Applicationクラスに@EnableAutoConfigurationまたは@SpringBootApplication設定がされている場合、ランタイム時に@Configurationが設定されたSpringAsyncConfigクラス(以下で作成予定)のthreadPoolTaskExecutorビーン情報を読み込むためです。
coreとmaxサイズを設定できます。この時、最初にcoreサイズ分動作し、これ以上処理できなくなった場合、maxサイズ分スレッドが増えるだろうと予想しますが、そうではありません。
内部的にInteger.MAX_VALUEサイズのLinkedBlockingQueueを生成し、coreサイズ分のスレッドで処理できなくなった場合、Queueで待機します。Queueがいっぱいになると、その時にmaxサイズ分スレッドを生成して処理します。
この時、QueueサイズをInteger.MAX_VALUEに設定するのが負担な場合は、queueCapacityを設定できます。上記のように設定すると、最初に3つのスレッドで処理し、処理速度が遅くなると作業を100個サイズのQueueに格納し、それ以上のリクエストが来ると最大30個のスレッドを生成して作業を処理します。
スレッドプール設定が完了したら、@Asyncアノテーションが付いたメソッドで上記ビーンの名前を付ければ良いです。
もしスレッドプールの種類を複数設定したい場合は、threadPoolTaskExecutor()のようなビーン生成メソッドを複数作成し、@Async設定時に希望するスレッドプールビーンを挿入すれば良いです。
戻り値別返却される形式
戻り値がない場合
非同期で処理する必要があるメソッドが処理結果を渡す必要がない場合です。この場合は、@Asyncアノテーションの戻り値をvoidに設定すれば良いです。
戻り値がある場合
Future、ListenableFuture、CompletableFuture型を戻り値型として使用できます。非同期メソッドの返却形式をnew AsyncResult()で包めば良いです。
[Future]
future.get()はブロッキングを通じてリクエスト結果が来るまで待つ役割をします。そのため、非同期ブロッキング方式になり、パフォーマンスが良くありません。通常、Futureは使用しません。
[ListenableFuture]
ListenableFutureはコールバックを通じてノンブロッキング方式で作業を処理できます。addCallback()メソッドの最初の引数は作業完了コールバックメソッド、2番目の引数は作業失敗コールバックメソッドを定義すれば良いです。ちなみに、スレッドプールのcoreスレッドを3つに設定したため、“Task Start”メッセージが最初に3つ表示されることを確認できます。
[CompletableFuture]
ListenableFutureだけでもノンブロッキングロジックを実装できますが、コールバックの中にコールバックが必要な場合、コールバック地獄と呼ばれる非常に複雑なコードを招きます。
<span class="image-inline ck-widget" contenteditable="false"><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9f152db8-c015-43cd-bf13-85c594f5f218%2FUntitled.png?table=block&id=268ac0bc-ca7b-4bcb-b11b-ac611a5038b2&spaceId=b453bd85-cb15-44b5-bf2e-580aeda8074e&width=2000&userId=80352c12-65a4-4562-9a36-2179ed0dfffb&cache=v2" alt="Untitled" style="aspect-ratio:2000/1455;" width="2000" height="1455"></span>
もちろん、今回はCompletableFutureを詳しく扱うわけではないので、複雑なコールバックに対処するコードが気になる場合は、下記のソースのリンクを参照してください。
ListenableFutureのコールバック定義よりも可読性が向上し、ノンブロッキング機能を完璧に実行します。そのため、@Asyncを使用する際に戻り値が必要な場合は、CompletableFutureを使用することをお勧めします。
@Asyncのメリット
開発者はメソッドを同期方式で記述し、非同期方式を希望する場合、単に@Asyncアノテーションをメソッドの上に貼り付ければ良いです。そのため、同期、非同期について保守性の良いコードを作成できます。
@Asyncの注意点
@Async機能を使用するには、@EnableAsyncアノテーションを宣言する必要がありますが、この時、別途設定しないとプロキシモードで動作します。つまり、@Asyncアノテーションで動作する非同期メソッドは全てSpring AOPの制約事項をそのまま守ることになります。詳しい理由は「該当の投稿」を参照してください。
- privateメソッドに@Asyncを付けてもAOPは動作しません。
- 同じオブジェクト内のメソッド同士を呼び出す場合、AOPは動作しません。
コメント0