scala - 如何在遍历 Scala future 序列时减少上下文切换

标签 scala akka threadpool forkjoinpool

我有一个程序需要获取大量(约 300 条)记录并对它们进行一些操作。记录被缓存,所以它不需要 CPU,几乎不需要时间。但是,在 50% 的 CPU 利用率下,p95 延迟为 40 毫秒。查看堆栈跟踪时,线程大部分时间处于 Parked 状态,使用最多的方法是“java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)”。所以我猜大部分时间都用于上下文切换

 def getObject(id: Ing): Future[SomeObject] = ??? // Can make a call to DB, but usually cached
 val ids: Seq[Int] = ??? // 300 ints

 Future.iterate(ids)(getObject)

如何提高性能?我尝试过 Akka FastFuture,并尝试使用全局(工作窃取线程池)和 Akka 默认调度程序(ForkJoin)。

具体来说 1)如果我们假设某些 getObject 调用阻塞(缓存未命中),我该怎么办 2) 如果我们假设所有 getObject 调用都是从缓存提供的,我该怎么办(我可以预先启动缓存,并使用后台刷新)

附注 堆转储:https://heaphero.io/my-heap-report.jsp?p=YXJjaGl2ZWQvMjAyMC8wMi8xOC8tLWJhc2UtZm9yay56aXAtNC0yNi02Lmpzb24tLQ==

最佳答案

在使用 Scala Future 时避免上下文切换的巧妙技巧在于使用 parasitic 作为 ExecutionContext,它“从中窃取执行时间其他线程通过让它的 Runnables 在调用 execute 的线程上运行,然后在 所有 它的 Runnables 被执行后将控制权交还给调用者”。 parasitic 从 Scala 2.13 开始可用,但您可以通过查看 its code (here for version 2.13.1) 轻松理解它并将其移植到 2.13 之前的项目中。 .对于 2.13 之前的项目,一个朴素但有效的实现会简单地运行 Runnable,而不用在线程上分派(dispatch)它们,这就达到了目的,如以下代码片段所示:

object parasitic212 extends ExecutionContext {

  override def execute(runnable: Runnable): Unit =
    runnable.run()

  // reporting failures is left as an exercise for the reader
  override def reportFailure(cause: Throwable): Unit = ???

}

parasitic 实现当然更加微妙。要更深入地了解其使用的推理和一些注意事项,我建议您引用 the PR the introduced parasitic作为一个公开可用的 API(它已经实现但保留供内部使用)。

引用原始 PR 描述:

A synchronous, trampolining, ExecutionContext has been used for a long time within the Future implementation to run controlled logic as cheaply as possible.

I believe that there is a significant number of use-cases where it makes sense, for efficiency, to execute logic synchronously in a safe(-ish) way without having users to implement the logic for that ExecutionContext themselves—it is tricky to implement to say the least.

It is important to remember that ExecutionContext should be supplied via an implicit parameter, so that the caller can decide where logic should be executed. The use of ExecutionContext.parasitic means that logic may end up running on Threads/Pools that were not designed or intended to run specified logic. For instance, you may end up running CPU-bound logic on an IO-designed pool or vice versa. So use of parasitic is only advisable when it really makes sense. There is also a real risk of hitting StackOverflowErrors for certain patterns of nested invocations where a deep call chain ends up in the parasitic executor, leading to even more stack usage in the subsequent execution. Currently the parasitic ExecutionContext will allow a nested sequence of invocations at max 16, this may be changed in the future if it is discovered to cause problems.

the official documentation for parasitic 中的建议,建议您仅在执行的代码快速将控制权返回给调用者时使用它。以下是版本 2.13.1 引用的文档:

WARNING: Only ever execute logic which will quickly return control to the caller.

This ExecutionContext steals execution time from other threads by having its Runnables run on the Thread which calls execute and then yielding back control to the caller after all its Runnables have been executed. Nested invocations of execute will be trampolined to prevent uncontrolled stack space growth.

When using parasitic with abstractions such as Future it will in many cases be non-deterministic as to which Thread will be executing the logic, as it depends on when/if that Future is completed.

Do not call any blocking code in the Runnables submitted to this ExecutionContext as it will prevent progress by other enqueued Runnables and the calling Thread.

Symptoms of misuse of this ExecutionContext include, but are not limited to, deadlocks and severe performance problems.

Any NonFatal or InterruptedExceptions will be reported to the defaultReporter.

关于scala - 如何在遍历 Scala future 序列时减少上下文切换,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60273882/

相关文章:

scala - 内置无副作用模拟 while 循环的方法

scala - Salat:如何调试堆栈跟踪以了解案例类中的哪个字段导致异常

java - Akka JavaTestKit JUnit 消息未发送给默认/用户

java - 提前/快速启动 ScheduledThreadPoolExecutor 中的任务

scala - 构建基于 Actor 的系统的设计模式/最佳实践

scala - 如何处理发出 Future[T] 的源?

java - Akka 路由消息每个 id 具有单个运行实例

multithreading - Akka 中使用 Actor 模型的读写器锁

java - 线程在执行期间被扰乱

c++ - 捕获信号 C++