I am trying to learn cats-effect via its provided tutorial. I enhanced it a little so that i can see the context shifting. I noticed that if i use the IOApp
provided ContextShift
it does not really shift, i see computations carrying on on the same thread. But if i provide my own ContextShift
i see what i was expecting (a switch every 10 computations). What am i doing wrong for IOApp
import cats.effect._
import cats.implicits._
import scala.concurrent.ExecutionContext
import java.util.concurrent.Executors
object Foo {
def fib(n: Int, a: Long = 0, b: Long = 1)(implicit
cs: ContextShift[IO]
): IO[Long] =
IO.suspend {
if (n == 0) IO.pure(a)
else {
val next = fib(n - 1, b, a + b)
println(s"${Thread.currentThread().getName()}")
// Every 100 cycles, introduce a logical thread fork
if (n % 10 == 0)
cs.shift *> next
else
next
}
}
}
object Main extends App {
val pool = Executors.newFixedThreadPool(10)
val ec: ExecutionContext =
ExecutionContext.fromExecutorService(pool)
implicit val cs: ContextShift[IO] = cats.effect.IO.contextShift(ec)
val x = Foo.fib(100).unsafeRunSync()
pool.shutdown()
}
Where are you running? The ContextShift introduced by IOApp uses the default thread pool, maybe you are in an environment which by default uses a single thread?
The default thread pool from what I know has number of threads that is the equals to the number of cores on my machine which is more than 1 which is why I am puzzled
1 Like
I don’t think that shift
guarantees that the computation will be moved to a different thread. It introduces an async boundary which frees up the current thread and shifts the computation to the default thread pool. But if it was already running on that default thread pool and nothing else gets scheduled on the thread it was running on then it’s possible that the same thread will be picked to continue the computation on.
1 Like
If what you said is true then for my given example code the job should not switch to a new thread faithfully every 10 computations since it’s the only job running. It should continue to run on the same thread
It could be due to a different scheduling strategy in the threadpool of the default execution context and in a Executors.newFixedThreadPool
.
The default one uses a kind of ForkJoinPool
, while newFixedThreadPool
returns a ThreadPoolExecutor
.
1 Like
You can demonstrate this for yourself. If you swap
val pool = Executors.newFixedThreadPool(10)
for
val pool = Executors.newWorkStealingPool(10)
then once again the same thread will be reused.
Or even if you prestart all the threads of your fixed thread pool:
val pool = Executors.newFixedThreadPool(10).asInstanceOf[java.util.concurrent.ThreadPoolExecutor]
pool.prestartAllCoreThreads()
Now again the same thread gets reused much more often. It looks like a default fixed thread pool starts off with 0 threads and wants to build up to its maximum amount as quickly as possible. After the pool size is maxed out it’s not as eager to switch between threads anymore.
2 Likes