如何在没有 java.util.Timer 的情况下实现延迟的未来?

Posted

技术标签:

【中文标题】如何在没有 java.util.Timer 的情况下实现延迟的未来?【英文标题】:How to implement a delayed future without java.util.Timer? 【发布时间】:2021-06-21 00:30:36 【问题描述】:

我正在编写一个简单的函数来返回一个在给定延迟后完成的Future[Unit]

def delayedFuture(delay: FiniteDuration): Future[Unit] = 
  val promise = Promise[Unit]()
  val timerTask = new java.util.TimerTask 
    override def run(): Unit = promise.complete(Success(()))
  
  new java.util.Timer().schedule(timerTask, delay.toMillis)
  promise.future

这个实现可能可以工作,但我不喜欢为每次调用创建一个新的Timer,因为每个Timer 实例都会创建一个线程。我可以将Timer 实例作为参数传递给delayedFuture,但我不希望客户端代码知道Timer。所以我想我根本不能使用java.util.Timer

我想使用ExecutionContext 进行任务调度,也许可以像这样定义delayedFuture

def delayedFuture(delay: FiniteDuration)
                 (implicit ec: ExecutoionContext): Future[Unit] = ???

在没有java.util.Timer 的情况下像这样实现delayedFuture 的最简单方法是什么?

【问题讨论】:

确实有点复杂——说到定时器,有一些重要的因素会影响系统运行时的性能。支持什么精度?支持什么分辨率?计时器的可扩展性如何(即一次可以轻松处理多少个任务)?定时器的生命周期是如何管理的? (即如果计时器关闭,计划的事情会被执行吗?谁关闭了它?) 哇!这是很多问题。不确定我能回答他们... 我知道 :) 这也是为什么我还没有为 Scala Futures 添加内置 Timer 工具的原因。这是一个困难的问题空间——一般地解决。 确实——将事物变成全局变量会使它们的测试变得极其棘手。一个选项是使实现可实例化以进行测试并在其上执行测试。但这意味着将计时器与使用全局计时器的逻辑分开测试。除非在测试期间使用一些巧妙的技巧来用模拟(反射?)替换全局。 不要使用java.util.Timer。它早已被ScheduledExecutorService 取代。您可以创建一个实例,例如通过Executors.newScheduledThreadPool(int)Executors.newSingleThreadScheduledExecutor() 【参考方案1】:

您不需要为每次调用创建一个新的Timer。只需制作一个全球性的。

object Delayed 
  private val timer = new Timer
  def apply[T](delay: Duration)(task: => T): Future[T] = 
    val promise = Promise[T]()
    val tt = new TimerTask 
      override def run(): Unit = promise.success(task)
    
    timer.schedule(tt, delay.toMillis)
    promise.future
  

  def unit(delay: Duration) = apply[Unit](delay)  () 

然后,Delayed.unit(10 seconds) 会为您提供一个在 10 秒内满足的未来单元。

【讨论】:

嗯...我担心这段代码是不可测试的:((因为java.util.Timer使用系统时钟... @Michael 是的,这是一个示例,用于说明如何在每次需要计时器时不创建计时器实例。为了使其可测试,需要付出一些额外的努力(使其成为一个类,以timer 作为参数),然后让对象扩展它,传递系统计时器用于产品,并在测试中使用模拟计时器创建实例。相当标准的东西,没有火箭科学。 你是对的。它不应该很复杂。谢谢。【参考方案2】:

您可以使用 Java 的ScheduledExecutorServices 之一,例如创建一个单线程池并仅用于调度:

val svc = new ScheduledThreadPoolExecutor(1, (r: Runnable) => 
    val t = new Thread(r)
    t.setName("future-delayer")
    t.setDaemon(true) // don't hog the app with this thread existing
    t
)

def delayedFuture(d: FiniteDuration) = 
  val p = Promise[Unit]()
  svc.schedule(() => p.success(()), d.length, d.unit)
  p.future

【讨论】:

这个回复回答了这个问题,但我猜delayedFuture 应该接收ScheduledExecutorService 作为参数,但我不希望客户端代码知道ScheduledExecutorService。我应该澄清这个问题...... 我建议您将一个作为私有 val 隐藏在具有 delayedFuture 的对象中。你不能单独使用ExecutionContext。在monix async/effect 库中,我们推出了自己的Scheduler 类型,因此我们处理延迟的方式与我在这里发布的非常相似——我们将EC 与可以进行调度的东西结合起来,例如一个线程调度的执行器。 另外值得注意的是,futures 将快速切换到在下一次.map/.flatMap 调用时提供的 ExecutionContext,这样除了调度之外不会占用一个线程。 如果我有一个具有delayedFuture 的对象,我想我会使用java.util.Timer 据我了解,您建议创建object DelayedFutureFactory private val svc: ScheduledThreadPoolExecutor = ???; def delayedFuture(delay: FiniteDuration): Future[Unit] = ??? 好的。也许这就是解决方案...

以上是关于如何在没有 java.util.Timer 的情况下实现延迟的未来?的主要内容,如果未能解决你的问题,请参考以下文章

Java 如何实现这样的定时任务

定时任务,客户页面操作,使用java.util.timer(要求时间精确度不高的定时任务)

等待计时器在 Java 中完成

在Java中如何实现较为精确的定时器

java.util.Timer 之后释放资源的最佳方式是啥?

java.util.Timer简介