我有一个功能,通过网络查找Elephant
,返回Future[Option[Elephant]]
。返回值为Future
,以便在网络调用异步发生时,函数可以立即返回。它包含Option
,None
表示尚未提供,而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(...)
留下我的意思?
答案 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)
,那么您可能会因为没有充分理由而消耗大量线程。改进这一点的第一步可能是使用Promise
和ScheduledThreadPoolExecutor
(我不知道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