Monix Task.sleep和单线程执行

时间:2019-08-09 08:40:57

标签: scala monix

我试图理解Monix中的任务计划原则。 如预期的那样,以下代码(源:https://slides.com/avasil/fp-concurrency-scalamatsuri2019#/4/3)仅产生“ 1”。

  val s1: Scheduler = Scheduler(
    ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor()),
    ExecutionModel.SynchronousExecution)

  def repeat(id: Int): Task[Unit] =
    Task(println(s"$id ${Thread.currentThread().getName}")) >> repeat(id)

  val prog: Task[(Unit, Unit)] = (repeat(1), repeat(2)).parTupled

  prog.runToFuture(s1)

  // Output:
  // 1 pool-1-thread-1
  // 1 pool-1-thread-1
  // 1 pool-1-thread-1
  // ...

当我们将Task.sleep添加到repeat方法中

  def repeat(id: Int): Task[Unit] =
    Task(println(s"$id ${Thread.currentThread().getName}")) >>
      Task.sleep(1.millis) >> repeat(id)

输出更改为

// Output
// 1 pool-1-thread-1
// 2 pool-1-thread-1
// 1 pool-1-thread-1
// 2 pool-1-thread-1
// ...

这两个任务现在都在一个线程上同时执行!不错:) 合作生产开始了。这到底发生了什么?谢谢:)

编辑:Task.shift而不是Task.sleep也会发生同样的情况。

2 个答案:

答案 0 :(得分:0)

我不确定这是否是您要寻找的答案,但是就可以了:

尽管命名有其他暗示,Task.sleep不能与Thread.sleep之类的更传统方法进行比较。

Task.sleep实际上不是在线程上运行,而只是指示调度程序在经过的时间后运行回调。

这里有monix/TaskSleep.scala的一些代码段供您比较:

[...]

implicit val s = ctx.scheduler
val c = TaskConnectionRef()
ctx.connection.push(c.cancel)

c := ctx.scheduler.scheduleOnce(
  timespan.length,
  timespan.unit,
  new SleepRunnable(ctx, cb)
)

[...]

private final class SleepRunnable(ctx: Context, cb: Callback[Throwable, Unit]) extends Runnable {

  def run(): Unit = {
    ctx.connection.pop()
    // We had an async boundary, as we must reset the frame
    ctx.frameRef.reset()
    cb.onSuccess(())
  }
}

[...]

在执行回调(此处为cb)之前的一段时间内,您的单线程调度程序(此处为ctx.scheduler)可以简单地将其线程用于接下来要排队的计算。

这也解释了为什么采用这种方法是可取的,因为我们不会在睡眠间隔期间阻塞线程-浪费了更少的计算周期。

希望这会有所帮助。

答案 1 :(得分:0)

扩展Markus的答案。

作为一个心理模型(出于说明目的),您可以想象线程池就像一个堆栈。由于您只有一个执行程序线程池,因此它将首先尝试运行repeat1,然后再运行repeat2

在内部,一切都只是一个巨大的FlatMap。运行循环将根据执行模型安排所有任务。

发生的事情是,sleep调度了一个可运行到线程池的线程。它将可运行的(repeat1)推入堆栈的顶部,从而为repeat2运行提供了机会。 repeat2也会发生同样的事情。

请注意,默认情况下,Monix的执行模型将为每1024个平面图执行一个异步边界。