/ / Comment modifier cette implémentation pour utiliser un seul bloc de synchronisation au lieu de plusieurs? - scala, synchronisation, évaluation paresseuse, mémorisation

Comment modifier cette implémentation pour utiliser un seul bloc de synchronisation au lieu de plusieurs? - scala, synchronisation, évaluation paresseuse, mémorisation

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 № 1

En 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)

}