Skip to content

Instantly share code, notes, and snippets.

@monadplus
Last active July 4, 2019 09:28
Show Gist options
  • Save monadplus/010c39abbf2f5e411a7078701cdb50ea to your computer and use it in GitHub Desktop.
Save monadplus/010c39abbf2f5e411a7078701cdb50ea to your computer and use it in GitHub Desktop.
cats-effect.Async and context switching

Cats-effect Async.async has the following pitfall:

During the execution of async, the effect is guaranteed to run on the backer ContextShift required by the creation of the Async type class. The main issue is that, if the callback is running on its on blocking execution context, the IO will not switch back to the main execution context (or the one running before calling async). And the rest of your code will be executed on the blocking 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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment