J'ai des événements dans mon système et une fonction qui calcule la valeur en fonction des événements et de la dernière valeur calculée:
Lorsque j'appelle cette fonction, je veux ce comportement:
- S'il y a de nouveaux événements, il doit calculer la nouvelle valeur en utilisant les événements et la dernière valeur mémorisée, et mémoriser le résultat
- S'il n'y a pas de nouveaux événements, il devrait retourner la dernière valeur mémorisée
Voici l'implémentation que j'ai trouvée:
<!-- language: lang-scala -->
class EventBasedMemo[Value, Event](initialValue: => Value,
buildValue: (Value, Event) => Value,
nextEvent: Option[Event] = None) {
lazy val memoValue: Value =
nextEvent.fold(initialValue)(event => buildValue(initialValue, event))
def update(event: Event): EventBasedMemo[Value, Event] =
new EventBasedMemo(memoValue, buildValue, Option(event))
}
type Value = Int
type Event = String
var callsCount = 0
var memo = new EventBasedMemo[Value, Event](0, (value, event) => {
callsCount += 1
println(s"$callsCount $event")
value + 1
})
memo = memo.update("A")
memo = memo.update("B")
memo = memo.update("C")
memo.memoValue // 1 A 2 B 3 C
memo.memoValue
memo = memo.update("D")
memo.memoValue // 4 D
Cela fonctionne bien. Mais le problème est que lazy val
utilise la synchronisation sous le capot, il y a donc plusieurs synchronisations imbriquées pour les calculs, ce qui est mauvais.
Comment le modifier pour qu'il n'utilise qu'une seule synchronisation?
Je veux que la solution soit immuable.
Réponses:
0 pour la réponse № 1En utilisant la mémorisation manuelle, vous pouvez éviterblocs de synchronisation. Mais dans le scénario multithread, le cache peut être recalculé plusieurs fois. Il est sûr de le recalculer uniquement si le calcul n'a pas d'effets secondaires (pas d'instructions d'impression comme dans l'exemple).
class EventBasedMemo[Value, Event](initialValue: => Value,
buildValue: (Value, Event) => Value,
nextEvent: Option[Event] = None) {
var cache: Option[Value] = None
def memoValue(): Value = {
cache match {
case None =>
cache = Some(nextEvent.fold(initialValue)(event => buildValue(initialValue, event)))
cache.get
case Some(value) => value
}
}
def update(event: Event): EventBasedMemo[Value, Event] =
new EventBasedMemo(memoValue, buildValue, Option(event))
}
Si vous voulez vraiment avoir une valeur paresseuse avec une synchronisation rapide, jetez un œil aux verrous en lecture / écriture comme ReentrantReadWriteLock. Le code suivant n'est qu'une démonstration, pas sûr qu'il fonctionne correctement dans un environnement multithread.
import java.util.concurrent.locks.ReentrantReadWriteLock
class EventBasedMemo[Value, Event](initialValue: => Value,
buildValue: (Value, Event) => Value,
nextEvent: Option[Event] = None,
lock:ReentrantReadWriteLock = new ReentrantReadWriteLock()) {
var cache: Option[Value] = None
def memoValue(): Value = {
lock.readLock().lock()
val result = cache match {
case None =>
lock.readLock().unlock()
lock.writeLock().lock()
cache = Some(nextEvent.fold(initialValue)(event => buildValue(initialValue, event)))
lock.writeLock().unlock()
lock.readLock().lock()
cache.get
case Some(value) => value
}
lock.readLock().unlock()
result
}
def update(event: Event): EventBasedMemo[Value, Event] =
new EventBasedMemo(memoValue, buildValue, Option(event), lock)
}