scalaasynchronousfuturemonix

Converting monix.eval.Task to scala.concurrent.Future


I’m trying to reuse some module that uses monix.eval.Task for async tasks. My project uses scala.concurrent.Future.

I'm trying to understand the best way to convert it to Future with the least amount of damage.

The easiest way is to use an additional thread pool:

import monix.eval.Task
import monix.execution.Scheduler.Implicits.global

import scala.concurrent.Future

// Create a Monix Task
val monixTask: Task[String] = Task("Hello, Monix!")

// Convert the Task to a Future using toFuture
val scalaFuture: Future[String] = monixTask.toFuture

But I don't fully understand the performance implications.

  1. I already have scala.concurrent.ExecutionContext defined in the project. What are the implications of adding the global monix.execution.Scheduler?
  2. Where the tasks will actually be computed? With ExecutionContext or Scheduler? I'm guessing it queue up on Scheduler, have a small overhead of conversion, and then run on ExecutionContext?

The given module simply uses Task.deferFuture on all the Future he receives.

The async tasks are IO tasks, having 300k~600k rpm.


Solution

  • Monix's default Scheduler delegates to the Scala default ExecutionContext, so if you were trying to avoid using that EC, you should define your own Scheduler instance that uses your custom EC. The Scheduler companion has a handful of apply methods defined for this purpose.

    A Scheduler uses a ScheduledExecutorService for scheduling tasks, and an ExecutionContext for running the tasks. So if you pass your custom ExecutionContext to the Scheduler.apply, your tasks will run there.

    A note about interop between Task and Future: a Task is lazy by nature. Constructing one does not run it. So when wrapping a Future-returning expression as a Task, the deferFuture takes the expression in call-by-name style, so that the Future will not actually be constructed and started until that Task is told to run.

    Since you mentioned the Tasks contain IO (I'm assuming you mean in the "I/O" sense, not cats.effect.IO), it might be appropriate to set up a separate dedicated thread pool (ExecutionContext) for performing IO-bound operations, so those don't block your CPU-bound "compute" threads. See Scheduler.io as a starting point for that.