Cats Effect ContextShift

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