概要 / 説明
従来、アプリケーションの処理は実行を行い結果を受け取るという同期的な動き方で多くのところで充分でした。
ところが近年はスマートフォンなどのモバイル機器やプロセッサのマルチコア化などの進歩に伴い、
複数の処理を同時に行えるような非同期処理も求められるようになってきています。
Spring では、@Async
アノテーションを使用して簡単に別スレッド上での非同期処理を行えます。
@Async
を利用した非同期処理の作り方を確認してみます。
非同期処理
Java で何らかの命令を実行する場合、スレッドの中で処理が行われます。
シングルスレッドの場合は、命令をスレッド上で逐次順番に処理を行っていきます。
そのため、同時処理はできません。
そこで、複数のスレッドを用意して各スレッド上で命令を実行し、
終了を待たずに別のスレッドで命令を実行して非同期的に並行処理を実現します。
前提 / 環境
ランタイムバージョン
- Kotlin : 1.3.0
- SpringBoot : 2.1.0.RELEASE
Spring Dependencies
- Web
- Actuator
開発環境
- OS : Mac
- IDE : IntelliJ IDEA
- Build : Gradle
手順 / 解説
シンプルな非同期処理
非同期処理の有効化
Spring Boot アプリケーションの中で非同期処理を利用できるようにする設定を行います。
設定内容は、コンフィグレーションクラス (@Configuration
アノテーション、または@SpringBootApplication
アノテーションの付与されたクラス)に @EnableAsync
アノテーションを追加するのみです。
@SpringBootApplication
@EnableAsync
class SimpleApplication
非同期処理の実装
別スレッドをの中で非同期処理を実施したい関数に対して @Async
アノテーションを付与します。
この@Async
アノテーションが付与された関数が非同期処理対象の関数として扱われ、処理される命令が別スレッドの中で実行されます。
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit
@Service("Async Task Service")
class AsyncTaskService {
val logger = LoggerFactory.getLogger(this::class.java.name)
@Async
fun standardTask() {
logger.info("Task Start")
TimeUnit.SECONDS.sleep(5)
logger.info("Task End")
}
}
非同期処理の呼び出し
@Async
アノテーションにより非同期処理を担当するクラスを呼び出す REST コントローラを配置します。
アクセスエンドポイントは、/async
として設定します。
これによりこの非同期処理は、http://localhost:8080/async
で呼び出す事ができます。
@RestController
@RequestMapping("/async")
class AsyncTaskController {
@GetMapping
fun callStandardTask() = service.standardTask()
}
非同期処理の実行
実際に非同期処理を5回連続で呼び出してみます。
$ curl http://localhost:8080/async
$ curl http://localhost:8080/async
$ curl http://localhost:8080/async
$ curl http://localhost:8080/async
$ curl http://localhost:8080/async
下記のように各非同期処理の開始と終了の実行時ログが標準出力に表示されます。
2018-12-19 20:50:22.662 INFO 22121 --- [cTaskExecutor-1] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 20:51:00.545 INFO 22121 --- [cTaskExecutor-2] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 20:51:01.886 INFO 22121 --- [cTaskExecutor-3] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 20:51:04.397 INFO 22121 --- [cTaskExecutor-4] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 20:51:05.550 INFO 22121 --- [cTaskExecutor-5] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 20:51:06.887 INFO 22121 --- [cTaskExecutor-1] i.p.s.simple.service.AsyncTaskService : Normal Task End
2018-12-19 20:51:09.400 INFO 22121 --- [cTaskExecutor-2] i.p.s.simple.service.AsyncTaskService : Normal Task End
2018-12-19 20:52:03.338 INFO 22121 --- [cTaskExecutor-3] i.p.s.simple.service.AsyncTaskService : Normal Task End
2018-12-19 20:52:05.123 INFO 22121 --- [cTaskExecutor-4] i.p.s.simple.service.AsyncTaskService : Normal Task End
2018-12-19 20:52:06.290 INFO 22121 --- [cTaskExecutor-5] i.p.s.simple.service.AsyncTaskService : Normal Task End
この結果を見ると、 cTaskExecutor-1
から cTaskExecutor-5
までの 5スレッドが生成され処理され、それぞれ非同期に処理されている事が確認できます。
スレッドプールを利用した非同期処理
先に実施した非同期処理では、呼び出し分だけスレッドが生成されました。
もし、これが非常に高頻度で呼び出しが行われるアプリケーションだったらどうなるでしょうか。
スレッドが多数生成され、システムリソースを枯渇させてパフォーマンスの低下や、最悪の場合システムダウンという事も考えられます。
Java EE アプリケーションサーバであれば、スレッド数の制御をプラットフォームとして制御する機能を提供している製品が多数あります。
では、今回のようなJava EE アプリケーションサーバを利用しない Spring Boot アプリケーションの場合はどうしたらよいでしょうか。
ThreadPoolTaskExecutor の定義
Spring で非同期処理を実施する場合、@Async
アノテーションを付与する事でデフォルトでは SimpleAsyncTaskExecutor
が使用され別スレッドを生成し非同期処理が行われています。
一方で、スレッド数の制御を行うためにスレッドプールの設定を行いたい場合は、ThreadPoolTaskExecutor
を構成して使用します。
@EnableAsync
を付与したコンフィグレーションクラス内に定義を行います。
以下では、二種類のプールを作成しています。
@SpringBootApplication
@EnableAsync
class SimpleApplication {
@Bean
fun normalTaskExecutor(): TaskExecutor = ThreadPoolTaskExecutor().apply {
corePoolSize = 1
setQueueCapacity(5)
maxPoolSize = 1
setThreadNamePrefix("NormalThread-")
setWaitForTasksToCompleteOnShutdown(true)
}
@Bean
fun prioritizedTaskExecutor(): TaskExecutor = ThreadPoolTaskExecutor().apply {
corePoolSize = 5
setQueueCapacity(5)
maxPoolSize = 5
setThreadNamePrefix("PrioritizedThread-")
setWaitForTasksToCompleteOnShutdown(true)
}
}
以下の属性を設定してスレッドプールの構成を行います。
名前 | 内容 |
---|---|
corePoolSize | この設定値までスレッド数を作成 |
setQueueCapacity | corePoolSize 数を超えると、この設定値までキューイング |
maxPoolSize | setQueueCapacity の最大までキューイングすると、この設定値までスレッド数を作成 |
ThreadPoolTaskExecutor を指定した非同期処理の実装
以下のように @Async
アノテーションで ThreadPoolTaskExecutor
の Bean名を明示的に指定し、
どのThreadPoolTaskExecutorによる非同期処理化を決定します。
@Async("prioritizedTaskExecutor")
fun prioritizedTask() {
logger.info("Prioritized Task Start")
TimeUnit.SECONDS.sleep(5)
logger.info("Prioritized Task End")
}
@Async("normalTaskExecutor")
fun normalTask() {
logger.info("Normal Task Start")
TimeUnit.SECONDS.sleep(5)
logger.info("Normal Task End")
}
非同期処理の実行
スレッドプール数を 1
に設定している非同期処理の結果を見てみます。
$ curl http://localhost:8080/async/normal
$ curl http://localhost:8080/async/normal
$ curl http://localhost:8080/async/normal
$ curl http://localhost:8080/async/normal
$ curl http://localhost:8080/async/normal
以下のように、NormalThread-1
スレッドのみで処理が行われている事が分かります。
指定した通り、スレッド数 1
で処理されています。
2018-12-19 22:30:17.654 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 22:30:22.658 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task End
2018-12-19 22:30:24.437 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 22:30:29.441 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task End
2018-12-19 22:30:29.441 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 22:30:34.442 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task End
2018-12-19 22:30:34.443 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 22:30:39.445 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task End
2018-12-19 22:30:39.446 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task Start
2018-12-19 22:30:44.455 INFO 22746 --- [ NormalThread-1] i.p.s.simple.service.AsyncTaskService : Normal Task End
次に、スレッドプール数を 5
にしている非同期処理を見てみます。
$ curl http://localhost:8080/async/high
$ curl http://localhost:8080/async/high
$ curl http://localhost:8080/async/high
$ curl http://localhost:8080/async/high
$ curl http://localhost:8080/async/high
こちらは、HighThread-1
から HighThread-5
までの 5スレッドが生成され使用されている事が分かります。
このように簡単にスレッドプールの制御も考慮した非同期処理ができる事が分かります。
2018-12-19 22:37:55.784 INFO 22746 --- [HighThread-1] i.p.s.simple.service.AsyncTaskService : Prioritized Task Start
2018-12-19 22:37:57.469 INFO 22746 --- [HighThread-2] i.p.s.simple.service.AsyncTaskService : Prioritized Task Start
2018-12-19 22:37:57.898 INFO 22746 --- [HighThread-3] i.p.s.simple.service.AsyncTaskService : Prioritized Task Start
2018-12-19 22:37:58.956 INFO 22746 --- [HighThread-4] i.p.s.simple.service.AsyncTaskService : Prioritized Task Start
2018-12-19 22:37:59.582 INFO 22746 --- [HighThread-5] i.p.s.simple.service.AsyncTaskService : Prioritized Task Start
2018-12-19 22:38:00.787 INFO 22746 --- [HighThread-1] i.p.s.simple.service.AsyncTaskService : Prioritized Task End
2018-12-19 22:38:02.473 INFO 22746 --- [HighThread-2] i.p.s.simple.service.AsyncTaskService : Prioritized Task End
2018-12-19 22:38:02.900 INFO 22746 --- [HighThread-3] i.p.s.simple.service.AsyncTaskService : Prioritized Task End
2018-12-19 22:38:03.957 INFO 22746 --- [HighThread-4] i.p.s.simple.service.AsyncTaskService : Prioritized Task End
2018-12-19 22:38:04.586 INFO 22746 --- [HighThread-5] i.p.s.simple.service.AsyncTaskService : Prioritized Task End
まとめ / 振り返り
非同期処理は、Webアプリケーションだけでなく、バッチアプリケーションなどでも有効に使える処理です。
このように簡単にスレッド生成やスレッドプールの制御も行いながらの非同期処理ができるので、利用できる場面がたくさん考えられそうです。