I have SimulationService
that holds simple logic to loop until a stop()
function has been called. When using Spring Boot Rest that works fine - since web requests to a spring application are scoped into threads and therefore can "interrupt" the main thread running in a loop by changing state - in this case a simple boolean flag that stops the while loop.
Now I wanted to test this behavior for a reason:
internal class SimulationServiceTest {
private val simulateservice = SimulationService()
@Test
fun `should update the amount of ticks ran by the simulation`(): Unit = runBlocking {
assertThat(SimulationContext.ticks).isEqualTo(0)
launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.IO) {
simulateservice.start()
}
sleep(1000)
launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.Default) {
simulateservice.stop()
}
assertThat(SimulationContext.ticks).isGreaterThan(0)
}
}
In this case I just want to test if the SimulationService
does keep track of the ticks or iterations the while loop has been gone through.
However I am not able to mimic the behavior of web requests with launch
or async
.
It behaves just like it does when I use it without CoRoutines.
When start()
is called - the main thread is stuck in a loop. Therefore I need a CoRoutine that launches to actually access that thread.
I read about CoRoutines not being multiple different threads. However I read that for CoRoutines there is at least one new thread created - otherwise there wouldn't be a way to become asynchronous.
I though that Dispatchers
represent the "new threads". So CoRoutine can run on the DEFAULT, MAIN and IO thread. For me that is DEFAULT & IO shouldn't be the same, right?
How is it happening now - that my CoRoutines still seem to be async onto one thread? The test process is running in indefinitely just like it would act on a common thread.
Here is an example how it does with with Java Executors
thread service:
internal class SimulationServiceTest {
private val simulateservice = SimulationService()
@Test
fun `should update the amount of ticks ran by the simulation`() {
assertThat(SimulationContext.ticks).isEqualTo(0)
val executor = Executors.newSingleThreadExecutor()
executor.submit {
simulateservice.start()
}
sleep(1000)
simulateservice.stop()
assertThat(SimulationContext.ticks).isGreaterThan(0)
}
}
CodePudding user response:
However I read that for CoRoutines there is at least one new thread created - otherwise there wouldn't be a way to become asynchronous.
What makes you think that? It is entirely possible to run asynchronous stuff on a single thread. JavaScript is basically single-threaded (if you forget about NodeJS's IO handling for a moment), and it has a ton of asynchronous primitives and libraries.
In the coroutines world, you can launch
a coroutine in the same thread as the current one, it will just wait for its turn to run (like in an event loop). The execution of the coroutines will be interlaced, and the points where the (single) thread switches from one coroutine to another are called suspension points (roughly, these are points where you call other suspend functions).
I though that Dispatchers represent the "new threads". So CoRoutine can run on the DEFAULT, MAIN and IO thread.
Dispatchers are an abstraction over how the coroutines are run. They can indeed represent thread pools on which you dispatch your tasks, but there is no 1-to-1 mapping between a dispatcher and threads. Dispatchers.Default
and Dispatchers.IO
are each backed by a pool of threads. The Default
thread pool is sized based on your CPU cores, whereas the IO
one can grow as necessary (within some reasonable limit).
For me that is DEFAULT & IO shouldn't be the same, right?
That is actually not entirely true, because some optimizations regarding those built-in dispatchers allow them to share some threads to avoid wasting resources unnecessarily. You shouldn't rely on the fact that these threads are the same or different. But it doesn't matter much in your case anyway.
How is it happening now - that my CoRoutines still seem to be async onto one thread? The test process is running in indefinitely just like it would act on a common thread.
In your current test code, you are not actually dispatching on the Default
and IO
dispatchers, because you're running them with CoroutineStart.UNDISPATCHED. Therefore, you are running your coroutines in the current thread until their first suspension point.
My guess is that start()
is not even a suspend function itself, and there is no suspension there to release the current thread and give control back to the test's body, so the sleep()
never gets a chance to run. But in order to confirm that we would need to see the code of the service, and more specifically the start()
method.