当我们尝试从actor的receive方法中启动一些期货时,我们发现了一种奇怪的行为。 如果我们使用配置的调度程序作为ExecutionContext,则期货在同一个线程上按顺序运行。如果我们使用ExecutionContext.Implicits.global,期货将按预期并行运行。
我们将代码简化为以下示例(下面是一个更完整的示例):
implicit val ec = context.getDispatcher
Future{ doWork() } // <-- all running parallel
Future{ doWork() }
Future{ doWork() }
Future{ doWork() }
Future {
Future{ doWork() }
Future{ doWork() } // <-- NOT RUNNING PARALLEL!!! WHY!!!
Future{ doWork() }
Future{ doWork() }
}
可编辑的例子是这样的:
import akka.actor.ActorSystem
import scala.concurrent.{ExecutionContext, Future}
object WhyNotParallelExperiment extends App {
val actorSystem = ActorSystem(s"Experimental")
// Futures not started in future: running in parallel
startFutures(runInFuture = false)(actorSystem.dispatcher)
Thread.sleep(5000)
// Futures started in future: running in sequentially. Why????
startFutures(runInFuture = true)(actorSystem.dispatcher)
Thread.sleep(5000)
actorSystem.terminate()
private def startFutures(runInFuture: Boolean)(implicit executionContext: ExecutionContext): Unit = {
if (runInFuture) {
Future{
println(s"Start Futures on thread ${Thread.currentThread().getName()}")
(1 to 9).foreach(startFuture)
println(s"Started Futures on thread ${Thread.currentThread().getName()}")
}
} else {
(11 to 19).foreach(startFuture)
}
}
private def startFuture(id: Int)(implicit executionContext: ExecutionContext): Future[Unit] = Future{
println(s"Future $id should run for 500 millis on thread ${Thread.currentThread().getName()}")
Thread.sleep(500)
println(s"Future $id finished on thread ${Thread.currentThread().getName()}")
}
}
我们尝试使用thread-pool-executor和fork-join-executor,结果相同。
我们是否以错误的方式使用期货? 那么你应该如何产生并行任务呢?
答案 0 :(得分:2)
来自Akka的内部BatchingExecutor
(强调我的)的描述:
执行者的混合特征,将多个嵌套
Runnable.run()
调用分组到一个传递给原始执行者的Runnable 。这可能是一个有用的优化,因为它绕过原始上下文的任务队列并将相关(嵌套)代码保存在单个线程上,这可以提高CPU亲和性。但是,如果传递给Executor的任务是阻塞或昂贵的,那么这种优化可以防止工作窃取并使性能变得更糟......如果代码不应该使用scala.concurrent.blocking
,那么批处理执行程序可以创建死锁,因为任务在其他任务中创建将阻止外部任务完成。
如果您正在使用混合在BatchingExecutor
中的调度程序 - 即MessageDispatcher
的子类 - 您可以使用scala.concurrent.blocking
构造来启用与嵌套Futures的并行性:< / p>
Future {
Future {
blocking {
doBlockingWork()
}
}
}
在您的示例中,您需要在blocking
方法中添加startFuture
:
private def startFuture(id: Int)(implicit executionContext: ExecutionContext): Future[Unit] = Future {
blocking {
println(s"Future $id should run for 500 millis on thread ${Thread.currentThread().getName()}")
Thread.sleep(500)
println(s"Future $id finished on thread ${Thread.currentThread().getName()}")
}
}
通过以上更改运行startFutures(true)(actorSystem.dispatcher)
的示例输出:
Start Futures on thread Experimental-akka.actor.default-dispatcher-2
Started Futures on thread Experimental-akka.actor.default-dispatcher-2
Future 1 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-2
Future 3 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-3
Future 5 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-6
Future 7 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-7
Future 4 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-5
Future 9 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-10
Future 6 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-8
Future 8 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-9
Future 2 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-4
Future 1 finished on thread Experimental-akka.actor.default-dispatcher-2
Future 3 finished on thread Experimental-akka.actor.default-dispatcher-3
Future 5 finished on thread Experimental-akka.actor.default-dispatcher-6
Future 4 finished on thread Experimental-akka.actor.default-dispatcher-5
Future 8 finished on thread Experimental-akka.actor.default-dispatcher-9
Future 7 finished on thread Experimental-akka.actor.default-dispatcher-7
Future 9 finished on thread Experimental-akka.actor.default-dispatcher-10
Future 6 finished on thread Experimental-akka.actor.default-dispatcher-8
Future 2 finished on thread Experimental-akka.actor.default-dispatcher-4
答案 1 :(得分:1)
它与调度员的“吞吐量”设置有关。我在application.conf中添加了一个“公平调度程序”来演示:
fair-dispatcher {
# Dispatcher is the name of the event-based dispatcher
type = Dispatcher
# What kind of ExecutionService to use
executor = "fork-join-executor"
# Configuration for the fork join pool
fork-join-executor {
# Min number of threads to cap factor-based parallelism number to
parallelism-min = 2
# Parallelism (threads) ... ceil(available processors * factor)
parallelism-factor = 2.0
# Max number of threads to cap factor-based parallelism number to
parallelism-max = 10
}
# Throughput defines the maximum number of messages to be
# processed per actor before the thread jumps to the next actor.
# Set to 1 for as fair as possible.
throughput = 1
}
以下是您对Futures使用公平调度程序进行一些修改并打印吞吐量设置当前值的示例:
package com.test
import akka.actor.ActorSystem
import scala.concurrent.{ExecutionContext, Future}
object WhyNotParallelExperiment extends App {
val actorSystem = ActorSystem(s"Experimental")
println("Default dispatcher throughput:")
println(actorSystem.dispatchers.defaultDispatcherConfig.getInt("throughput"))
println("Fair dispatcher throughput:")
println(actorSystem.dispatchers.lookup("fair-dispatcher").configurator.config.getInt("throughput"))
// Futures not started in future: running in parallel
startFutures(runInFuture = false)(actorSystem.dispatcher)
Thread.sleep(5000)
// Futures started in future: running in sequentially. Why????
startFutures(runInFuture = true)(actorSystem.dispatcher)
Thread.sleep(5000)
actorSystem.terminate()
private def startFutures(runInFuture: Boolean)(implicit executionContext: ExecutionContext): Unit = {
if (runInFuture) {
Future{
implicit val fairExecutionContext = actorSystem.dispatchers.lookup("fair-dispatcher")
println(s"Start Futures on thread ${Thread.currentThread().getName()}")
(1 to 9).foreach(i => startFuture(i)(fairExecutionContext))
println(s"Started Futures on thread ${Thread.currentThread().getName()}")
}
} else {
(11 to 19).foreach(startFuture)
}
}
private def startFuture(id: Int)(implicit executionContext: ExecutionContext): Future[Unit] = Future{
println(s"Future $id should run for 500 millis on thread ${Thread.currentThread().getName()}")
Thread.sleep(500)
println(s"Future $id finished on thread ${Thread.currentThread().getName()}")
}
}
输出:
Default dispatcher throughput:
5
Fair dispatcher throughput:
1
Future 12 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-3
Future 11 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-4
Future 13 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-2
Future 14 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-5
Future 16 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-7
Future 15 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-6
Future 17 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-8
Future 18 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-9
Future 19 should run for 500 millis on thread Experimental-akka.actor.default-dispatcher-10
Future 13 finished on thread Experimental-akka.actor.default-dispatcher-2
Future 11 finished on thread Experimental-akka.actor.default-dispatcher-4
Future 12 finished on thread Experimental-akka.actor.default-dispatcher-3
Future 14 finished on thread Experimental-akka.actor.default-dispatcher-5
Future 16 finished on thread Experimental-akka.actor.default-dispatcher-7
Future 15 finished on thread Experimental-akka.actor.default-dispatcher-6
Future 17 finished on thread Experimental-akka.actor.default-dispatcher-8
Future 18 finished on thread Experimental-akka.actor.default-dispatcher-9
Future 19 finished on thread Experimental-akka.actor.default-dispatcher-10
Start Futures on thread Experimental-akka.actor.default-dispatcher-10
Future 1 should run for 500 millis on thread Experimental-fair-dispatcher-12
Future 2 should run for 500 millis on thread Experimental-fair-dispatcher-13
Future 4 should run for 500 millis on thread Experimental-fair-dispatcher-15
Future 3 should run for 500 millis on thread Experimental-fair-dispatcher-14
Future 5 should run for 500 millis on thread Experimental-fair-dispatcher-17
Future 6 should run for 500 millis on thread Experimental-fair-dispatcher-16
Future 7 should run for 500 millis on thread Experimental-fair-dispatcher-18
Future 8 should run for 500 millis on thread Experimental-fair-dispatcher-19
Started Futures on thread Experimental-akka.actor.default-dispatcher-10
Future 4 finished on thread Experimental-fair-dispatcher-15
Future 2 finished on thread Experimental-fair-dispatcher-13
Future 1 finished on thread Experimental-fair-dispatcher-12
Future 9 should run for 500 millis on thread Experimental-fair-dispatcher-15
Future 5 finished on thread Experimental-fair-dispatcher-17
Future 7 finished on thread Experimental-fair-dispatcher-18
Future 8 finished on thread Experimental-fair-dispatcher-19
Future 6 finished on thread Experimental-fair-dispatcher-16
Future 3 finished on thread Experimental-fair-dispatcher-14
Future 9 finished on thread Experimental-fair-dispatcher-15
正如您所看到的,公平调度员在大多数期货中使用不同的线程。
默认调度程序针对actor进行了优化,因此将吞吐量设置为5以最小化上下文切换以提高消息处理吞吐量,同时保持一定程度的公平性。
我的公平调度程序中唯一的变化是吞吐量:1,即如果可能,每个异步执行请求都有自己的线程(最多为parallelism-max)。
我建议为不同用途的期货创建单独的调度程序。例如。一个调度程序(即线程池)用于调用某些Web服务,另一个用于阻止数据库访问等。这可以通过调整自定义调度程序设置来更精确地控制它。
看一下https://doc.akka.io/docs/akka/current/dispatchers.html,它对于理解细节非常有用。
同时查看Akka参考设置(特别是默认调度员),那里有很多有用的评论:https://github.com/akka/akka/blob/master/akka-actor/src/main/resources/reference.conf
答案 2 :(得分:0)
经过一番研究后,我发现Dispatcher
类实现akka.dispatch.BatchingExecutor
。出于性能原因,此类检查应在同一线程上批处理哪些任务。 Future.map
在内部创建scala.concurrent.OnCompleteRunnable
,该BatchingExecutor
在map()
。
flatMap()
/ Future.apply
这似乎是合理的,其中一个任务生成一个后续任务,但不适用于用于分叉工作的显式新Futures。
在内部,Future.successful().map
由object MyFuture {
def apply[T](body: =>T)(implicit executor: ExecutionContext): Future[T] = {
val promise = Promise[T]()
class FuturesStarter extends Runnable {
override def run(): Unit = {
promise.complete(Try(body))
}
}
executor.execute(new FuturesStarter)
promise.future
}
}
实现,因此被批量处理。我的解决方法是以不同的方式创建未来:
FutureStarter
Future
- Runnables没有批处理,因此并行运行。
有人可以确认这个解决方案没问题吗?
有没有更好的方法来解决这个问题?
需要BatchingExecutor
/ moduleForComponent
的当前实现,还是错误?