Scala中的尾递归,功能轮询

时间:2017-05-23 23:58:36

标签: scala tail-recursion

我有一个功能,通过网络查找Elephant,返回Future[Option[Elephant]]。返回值为Future,以便在网络调用异步发生时,函数可以立即返回。它包含OptionNone表示尚未提供,而Some表示已找到大象:

def checkForElephant : Future[Option[Elephant]] = ???

我想做的是编写一个名为pollForElephant的函数。

def pollForElephant : Future[Elephant] = ???

此函数应返回Future进程,该进程将调用checkForElephant并在第一次检查时找到元素后快速成功,但之后每10秒再次检查一次,直到{{1}找到了,即使没有大象也必须永远尝试。

执行此操作的简单方法是强制检查同步,在Elephant域之外编写递归函数进行轮询,然后在整个事件中创建Future:< / p>

Future

这似乎非常不优雅,从import scala.annotation.tailrec import scala.concurrent.{Await,Future,blocking} import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global class Elephant; def checkForElephant : Future[Option[Elephant]] = ??? def synchronousCheckForElephant : Option[Elephant] = blocking { Await.result( checkForElephant, Duration.Inf ) } @tailrec def poll( last : Option[Elephant] ) : Elephant = { last match { case Some( elephant ) => elephant case None => { blocking { Thread.sleep( 10.seconds.toMillis ) } poll( synchronousCheckForElephant ) } } } def pollForElephant : Future[Elephant] = Future { poll( synchronousCheckForElephant ) } 域开始,强制进入同步,然后返回。我以为我应该可以从Future做所有事情。所以,我试过这个:

Future

不幸的是,正如上面的评论所说,import scala.annotation.tailrec import scala.concurrent.{Await,Future,blocking} import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global class Elephant; def checkForElephant : Future[Option[Elephant]] = ??? // oops! this is not @tailrec def poll( last : Future[Option[Elephant]] ) : Future[Elephant] = { last.flatMap { mbElephant => mbElephant match { case Some( elephant ) => Future.successful( elephant ) case None => { blocking { Thread.sleep( 10.seconds.toMillis ) } poll( checkForElephant ) } } } } def pollForElephant : Future[Elephant] = poll( checkForElephant ) 函数不是尾递归的。大象可能需要很长时间才能到达,而且我应该无限期地等待,但堆栈可能会爆炸。

整个事情感觉有点奇怪。我应该回到更容易理解的同步方法吗?是否有一种安全的方式可以在poll(...)留下我的意思?

1 个答案:

答案 0 :(得分:1)

我同意@ PH88的评论:你不需要调用尾递归,因为在checkForElephant内你flatMap创建了一个新的Future,因而是新的堆栈。这是一个我试图模仿你checkForElephant的简单代码:

type Elephant = String
val rnd = new Random()

def checkForElephant: Future[Option[Elephant]] = Future({
  val success = rnd.nextDouble() < 0.2
  println(s"Call to checkForElephant => $success")
  if (success) Some(Thread.currentThread().getStackTrace().mkString("\n")) else None
})

def poll(last: Future[Option[Elephant]]): Future[Elephant] = {
  last flatMap {
      case Some(elephant) => Future.successful(elephant)
      case None => {
        blocking {
          println("Sleeping")
          Thread.sleep(100.millisecond.toMillis)
        }
        poll(checkForElephant)
      }
    }
 }

def pollForElephant: Future[Elephant] = poll(checkForElephant)

val result = Await.result(pollForElephant, Duration.Inf)
println(result)

这是运行中的一个输出:

  

致电checkForElephant =&gt;假   
睡觉   
调用checkForElephant =&gt;假   
睡觉   
调用checkForElephant =&gt;假   
睡觉   
调用checkForElephant =&gt;假   
睡觉   
调用checkForElephant =&gt;假   
睡觉   
调用checkForElephant =&gt;假   
睡觉   
调用checkForElephant =&gt;假   
睡觉   
调用checkForElephant =&gt;真正   
java.lang.Thread.getStackTrace(Thread.java:1556)   
so.TestApp $$ anonfun $所以$ $$ TestApp checkForElephant $ 1 $ 1.适用(TestApp.scala:97)   
so.TestApp $$ anonfun $所以$ $$ TestApp checkForElephant $ 1 $ 1.适用(TestApp.scala:94)   
scala.concurrent.impl.Future $ PromiseCompletingRunnable.liftedTree1 $ 1(Future.scala:24)   
scala.concurrent.impl.Future $ PromiseCompletingRunnable.run(Future.scala:24)   scala.concurrent.impl.ExecutionContextImpl $ AdaptedForkJoinTask.exec(ExecutionContextImpl.scala:121)   
scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)   scala.concurrent.forkjoin.ForkJoinPool $ WorkQueue.pollAndExecAll(ForkJoinPool.java:1253)   
scala.concurrent.forkjoin.ForkJoinPool $ WorkQueue.runTask(ForkJoinPool.java:1346)   
scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)   
scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

你可能会看到,尽管checkForElephant前7次返回None,但堆栈跟踪很浅。

<强>旁注

我不喜欢你的方法是你在睡眠10秒钟时阻止一些线程。这对我来说似乎效率低下。如果你想要有很多这样的调用,你可以考虑使用更聪明的东西,比如Java ScheduledThreadPoolExecutor或Akka Actors。

<强>更新

  

但它会泄漏内存,堆栈帧的逻辑等价物,作为堆上的对象维护吗?

不,除非你的checkForElephant中有一些非常奇怪的东西,否则不应该这样。为了有内存泄漏,某些内存应由某些“root”保留。可能的根是:静态变量,线程局部变量和堆栈。我们知道堆栈不会增长,因此它不能成为泄漏源。如果你没有搞乱静态和/或本地线程,你应该是安全的。

对于 线程消费 ,如果系统中只有一个“大象”,我认为没有什么比这更好了。但是,如果您的checkForElephant实际上是checkForElephant(id),那么您可能会因为没有充分理由而消耗大量线程。改进这一点的第一步可能是使用PromiseScheduledThreadPoolExecutor(我不知道Scala的等价物)并牺牲一些功能样式以便更好地使用线程,例如:

// Just 1 thread should be enough assuming checkForElephant schedules 
// it's Future on some executor rather than current thread
val scheduledExecutor = new ScheduledThreadPoolExecutor(1)

def pollForElephant: Future[Elephant] = {
  def scheduleDelayedPoll(p: Promise[Elephant]) = {
    scheduledExecutor.schedule(new Runnable {
      override def run() = poll(p)
    },
      10, TimeUnit.SECONDS)
  }

  def poll(p: Promise[Elephant]): Unit = {
    checkForElephant.onComplete {
      case s: Success[Option[Elephant]] => if (s.value.isDefined) p.success(s.value.get) else scheduleDelayedPoll(p)
      case f: Failure[_] => scheduleDelayedPoll(p)
    }
  }

  val p = Promise[Elephant]()
  poll(p)
  p.future
}

如果您有更多负载,下一步将使用checkForElephant的一些非阻塞I / O来阻止网络上的请求的线程。如果您实际使用的是Web服务,请查看Play WS API,它是AsyncHttpClient周围的Scala包装,而后者又基于Netty