Cats-effect Async.async
has the following pitfall:
During the execution of
async
, the effect is guaranteed to run on the backerContextShift
required by the creation of theAsync
type class. The main issue is that, if the callback is running on its onblocking execution context
, theIO
will not switch back to the main execution context (or the one running before callingasync
). And the rest of your code will be executed on theblocking execution context
. This could end up making your cpu bounded code run on a specialized thread pool that may not be ready to handle more threads running on it.
You can test it yourself by running the following snippet:
import cats._, cats.data._, cats.implicits._
import cats.effect._, cats.effect.implicits._
import fs2._, fs2.io._
import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
object Example extends IOApp {
def printEC =
IO(println(Thread.currentThread().getName()))
def asyncAction(cb: Either[Throwable, Unit] => Unit): Unit = {
implicit val ec = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1))
val future = Future(Thread.sleep(500))(ec)
future.onComplete(res => cb(res.toEither))
}
def run(args: List[String]): IO[ExitCode] =
(printEC >> IO.async[Unit] { cb =>
asyncAction(cb)
} >> printEC).as(ExitCode.Success)
}
The solution:
1.- As a user you must guarantee that, in case of having a cb that switches context, a ContextShift.shift
is called afterward every async
call.
2.- An ergonomic solution, proposed by Daniel Spiewak, is the following:
trait AsyncShift[F[_]] extends ContextShift[F] {
// Not sure about making it subtype of Async.
// It (maybe) won't break async laws but the async will have a different behaviour.
// Like Async.async but shifts execution after completition.
def async[A](k: (Either[Throwable, A] => Unit) => Unit): F[A]
}
This will be fixed on cats-effects 3.0.
Daniel Spiewak: "It's definitely not an optimal situation, and what you're running into is a very common pitfall for sure. CE 3 will move to scoped autoshifting behavior by default, which has some unfortunate performance and use-case implications (particularly for use-cases which are dependent on single dispatch thread models, such as GUI interactions), but it has proven to be saner and safer as a default."
Source: