同步多线程memoization的函数参数

时间:2014-06-20 04:55:53

标签: multithreading scala asynchronous memoization thread-synchronization

我的核心问题是:如何在对象实例和方法参数组合的方法中实现同步?

以下是我的情况详情。我正在使用以下代码来实现memoization,改编自this answer

/**
 * Memoizes a unary function
 * @param f the function to memoize
 * @tparam T the argument type
 * @tparam R the result type
 */
class Memoized[-T, +R](f: T => R) extends (T => R) {

  import scala.collection.mutable

  private[this] val cache = mutable.Map.empty[T, R]

  def apply(x: T): R = cache.getOrElse(x, {
    val y = f(x)
    cache += ((x, y))
    y
  })
}

在我的项目中,我正在记住Future以重复删除异步API调用。当使用for...yield映射由标准ExcecutionContext创建的结果期货时,这种情况正常,但是当我升级到Scala Async以更好地处理这些期货时。但是,我意识到库使用的多线程允许多个线程进入apply,从而打败memoization,因为async阻止所有并行执行,在cache之前输入“orElse”thunk使用新的Future进行更新。

要解决此问题,我将主要的apply函数放在this.synchronized块中:

def apply(x: T): R = this.synchronized {
  cache.getOrElse(x, {
    val y = f(x)
    cache += ((x, y))
    y
  })
}

这恢复了记忆行为。缺点是这将阻止具有不同参数的调用,至少在创建Future之前。我想知道是否有办法在Memoized实例和x参数值apply的组合上设置更细粒度的同步。这样,只会阻止重复数据删除的呼叫。

作为旁注,我不确定这是否真的对性能至关重要,因为一旦Future被创建并返回(我认为?),同步块将会释放。但是,如果我对此没有任何顾虑,我也想知道。

1 个答案:

答案 0 :(得分:1)

Akka演员结合期货提供了一种强大的方式来包裹可变状态而不会阻塞。这是一个如何使用Actor进行记忆的简单示例:

import akka.actor._
import akka.util.Timeout
import akka.pattern.ask
import scala.concurrent._
import scala.concurrent.duration._

class Memoize(system: ActorSystem) {
  class CacheActor(f: Any => Future[Any]) extends Actor {
    private[this] val cache = scala.collection.mutable.Map.empty[Any, Future[Any]]

    def receive = {
      case x => sender ! cache.getOrElseUpdate(x, f(x))
    }
  }

  def apply[K, V](f: K => Future[V]): K => Future[V] = {
    val fCast = f.asInstanceOf[Any => Future[Any]]
    val actorRef = system.actorOf(Props(new CacheActor(fCast)))
    implicit val timeout = Timeout(5.seconds)
    import system.dispatcher
    x => actorRef.ask(x).asInstanceOf[Future[Future[V]]].flatMap(identity)
  }
}

我们可以像:

一样使用它
val system = ActorSystem()
val memoize = new Memoize(system)
val f = memoize { x: Int =>
  println("Computing for " + x)
  scala.concurrent.Future.successful {
    Thread.sleep(1000)
    x + 1
  }
}
import system.dispatcher
f(5).foreach(println)
f(5).foreach(println)

"计算5"只打印一次,但" 6"将打印两次。

有一些可怕的asInstanceOf调用,但它完全是类型安全的。