There is a list of parameters each of which is an input for MongoDB query.
Queries might be different, but for simplicity let's keep only one encapsulated in callMongoReactive.
fun callMongoReactive(p: Param): Mono<Result>{
// ...
}
fun queryInParallel(parameters: List<Param>): List<Result> =
parameters
.map { async { mongo.findSomething(it).awaitSingle() } }
.awaitAll()
Suppose parameters list size is not greater than 20.
What is the optimal strategy of coroutine dispatchers usage that makes these async requests run in parallel simultaneously?
Should a custom dispatcher (with a dedicated thread pool) be created or is there a standard approach for such situations?
If you want a very general answer, I'd say you may or may not want to care at this level. You don't have to.
In this specific case, the original API is reactive (immediately returns Mono), which means the actual work is not going to be performed on the thread calling callMongoReactive/findSomething, but on whatever thread pool mongoDB decided to use to back this Mono. This means the choice of dispatcher in this case really doesn't matter.
So especially in this case, I'd go for the simplest option: don't choose. Use coroutineScope and expose a suspend function so the caller decides on the coroutine context (including the dispatcher):
suspend fun queryInParallel(parameters: List<Param>): List<Result> = coroutineScope {
parameters
.map { async { mongo.findSomething(it).awaitSingle() } }
.awaitAll()
}
This is the usual idiom for "parallel decomposition of work". I'd say it's the most common.
What is the optimal strategy of coroutine dispatchers usage that makes these async requests run in parallel simultaneously?
It's worth noting that the idiom above expresses concurrency, but whether the bodies of the asyncs will be run in parallel or not depends on the dispatcher chosen by the caller.
(To reiterate, the dispatcher only affects the body of those asyncs, and in this specific case they don't use the thread that much because they call a non-blocking method anyway, so it really doesn't matter.)
Now in cases where it does matter, any dispatcher backed by more than 1 thread would allow parallelism here. There are several existing dispatchers that may be useful, without needing to create your own thread pool. Dispatchers.Default has a number of threads adapted to the number of cores of the machine it's running on, so it's a good fit for CPU-bound work. Dispatchers.IO scales the number of threads as needed, which is useful if you have a lot of blocking IO and want to avoid starvation.
You can also use limitedParallelism on any dispatcher to get a view of it with only a limited number of threads, which may be useful in some cases where you don't want to create an extra thread pool, but you do want to limit the number of available threads more than what the built-in dispatchers offer.
Creating a custom thread pool can be interesting if you want to isolate parts of your system in case one subsystem misbehaves and starves threads. It does have a memory overhead, though, since you're creating more threads.