Protected
Protected

Software Transactional Memory (STM)

An STM turns the Java heap into a transactional data set with begin/commit/rollback semantics. Very much like a regular database. It implements the first three letters in ACID; ACI:

  • Atomic
  • Consistent
  • Isolated

Overview of STM


Akka’s STM implements the concept in Clojure’s STM view on state in general. Please take the time to read this excellent document and view this presentation by Rich Hickey (the genius behind Clojure), since it forms the basis of Akka’s view on STM and state in general.

It is based on two concepts:
  • Managed References: Memory cells, holding an immutable value, that implement CAS (Compare-And-Swap) semantics and are managed and enforced by the STM for coordinated changes across many References.
  • Persistent Datastructures: Immutable but with constant time access and modification. The use of structural sharing and an insert or update does not ruin the old structure, hence “persistent”. Makes working with immutable composite types fast.

The Persistent Datastructures consist of a Map and Vector and are Scala ports of Clojure’s Map and Vector. The Managed References are implemented using the excellent Multiverse STM.

Persistent Datastructures


Akka's STM only works with immutable data. This can be costly if you have large datastructures and are using a naive copy-on-write. In order to make working with immutable datastructures fast enough Akka provides what is called Persistent Datastructures. Akka currently has two different ones:

  • HashTrie (http://en.wikipedia.org/wiki/Trie ) - which implements 'scala.Map'
  • Vector - which implements 'scala.RandomAccessSeq'

These are Scala ports of Clojure's datastructures. They are immutable and each update creates a completely new version but they are using clever structural sharing in order to make them almost as fast, for both read and update, as regular mutable Maps.

This illustration is taken from Rich Hickey's presentation. Copyright Rich Hickey 2009.

external image clojure-trees.png

Persistent Vector


This is some of its API. It extends Scala's RandomAccessSeq:

def apply(i: Int): T
def +[A >: T](obj: A): Vector[A]
def pop: HashTrie[K, A] // remove tail
def update[A >: T](i: Int, obj: A): Vector[A]

Creating a Vector:

import se.scalablesolutions.akka.collection._
 
val vector = new Vector[T]

Persistent Map (HashTrie)


This is some of its API. It extends Scala's Map:

def get(key: K): V
def +[A >: V](pair: (K, A)): HashTrie[K, A]
def -(key: K): HashTrie[K, A]
def empty[A]: HashTrie[K, A]

Creating a HashTrie:

import se.scalablesolutions.akka.collection._
 
val hashTrie = new HashTrie[K, V]

Managed References


Managed References are memory cells that point to immutable data. The data, e.g. the Ref's value can never change, but the Ref can be swapped with some other immutable data. E.g. the Ref itself is mutable but only within a transaction. If you try to modify a Ref outside a transaction you will get an exception. Ref's separate identity from value.

Here is an example of how you can create a Ref. It is using one of the two persistent datastructures that Akka has; the HashTrie, which is an immutable Map but with near constant time access and modification operations.

val ref = TransactionalState.newRef(HashTrie[String, User]())
 
val users = ref.get
val newUsers = users + (“bill” -> new User(“bill”, “secret”) // creates new HashTrie
 
ref.swap(newUsers)

The Ref is monadic and is possible to use within a for-comprehension. Here are some examples:

val usersRef = TransactionalState.newRef(HashTrie[String, User]())
 
for (users <- usersRef) {
  users + (name -> user)
}
 
val user = for (users <- usersRef) yield {
  users(name)
}
 
for {
  users <- usersRef
  user  <- users
  roles <- rolesRef
  role  <- roles
  if user.hasRole(role)
} {
  ... // do stuff
}

You can also create Refs directly like this:

val ref = TransactionalRef()
ref.swap(..)

Managed Datastructures


Akka provides three different datastructures that are managed by the STM.
  • Map
  • Vector
  • Ref

Map and Vector look like regular mutable datastructures, they even implement the standard Scala 'Map' and 'RandomAccessSeq' interfaces. But they are implemented using persistent datastructures and managed references under the hood. Therefore they are safe to use in a concurrent environment. They can only be modified inside the scope of an STM transaction (see below for details).

These managed datastructures always have to be created within a message send since that will trigger a transaction. It means that you always have to either declare them as 'lazy' or do lazy initialization by some other means.

Here is how you create these:
// Scala version
lazy val map = TransactionalState.newMap[String, Person]
lazy val vector = TransactionalState.newVector[Address]
lazy val ref = TransactionalState.newRef[Account]

Managing Transactions


You can manage transactions in two different ways:

  • Using 'atomic' blocks
  • Using for-comprehensions (monadic API)

Atomic Block API


import se.scalablesolutions.akka.stm.Transaction._
 
atomic {
  .. // do something within a transaction
}
 
atomic(maxNrOfRetries) {
  .. // do something within a transaction
}
 
atomicReadOnly {
  .. // do something within a transaction
}
 
atomically {
  .. // try to do something
} orElse {
  .. // if tx clash; try do do something else
}

Monadic API using for-comprehensions


Update:

import se.scalablesolutions.akka.stm.Transaction
 
val userStorage = TransactionalState.newMap[String, User]
 
for (tx <- Transaction()) {
  userStorage.put(user.name, user) // transactional
}

Read:

import se.scalablesolutions.akka.stm.Transaction
 
val userStorage = TransactionalState.newMap[String, User]
 
val users = for {
  tx <- Transaction()
  name <- userNames
  if userStorage.contains(name)
} yield userStorage.get(name) // transactional

Using STM outside of Akka Actors

If you are using the STM constructs outside of an Actor, you need to provide it with an implicit TransactionFamilyName
def foo = {
  implicit val tfn = "uniquename" //TransactionFamilyName
 
  atomic {
    //Do stuff
  }
}

How to disable the STM?


By default the STM is always turned on. This means that Actors and Active Objects will have the transaction semantics of “transaction supports”. E.g. they will happily join an existing transaction but not start a new one.

If you would like to completely turn off the STM since you know that you are not using any Transactional datastructures at all then you can either invoke:
// Scala version
TransactionManagement.disableTransactions

Or you can turn it off in the ‘akka.conf’ configuration file.

<akka>
  <stm>
    service = off
  </stm>
</akka>

Turning it off will yield better performance since the STM adds a certain overhead with its bookkeeping.

How to tune the STM?


Akka's STM is based on Multiverse. Multiverse has a configuration file that you can edit to tune the STM.

Generally best performance is accomplished by setting these options (which are already set for you by the runtime system):
-Dorg.multiverse.MuliverseConstants.sanityChecks=false
-Dorg.multiverse.api.GlobalStmInstance.factorymethod=org.multiverse.stms.alpha.AlphaStm.fastDebug

Here is the full configuration file, which resides in 'AKKA_HOME/config'. You can use it to fine-tune the STM, to get Multiverse to dump the generated code, to perform sanity checks etc.

# ============================================
# ===== Multiverse JVM Options Reference =====
# ============================================
 
# All these properties can be set on the commandline using '-D<option-name>=<value>'
 
# a flag that is used to enable sanity checks.
# default=true
org.multiverse.MuliverseConstants.sanityChecks=<type:boolean>
 
# a flag that enables to dump of bytecode of the instrumented classes to the tmp directory
# This is very interesting feature for debugging of the instrumentation
# default=false
org.multiverse.stms.alpha.instrumentation.MultiverseJavaAgent.dumpBytecode=<type:boolean>
 
# a string containing the full path to a static no-arg factory method that is used to create the global stm.
# default = org.multiverse.stms.alpha.AlphaStm.createDebug
org.multiverse.api.GlobalStmInstance.factorymethod=<type:string>
 
# a flag that enables the reuse of the FailedToObtainLocksException exception instance
# default = true
org.multiverse.api.exceptions.FailedToObtainLocksException.reuse=<type:boolean>
 
# a flag that enables the reuse of the LoadLockedException exception instance
# default = true
org.multiverse.api.exceptions.LoadLockedException.reuse=<type:boolean>
 
# a flag that enables the reuse of the LoadLockedException exception instance
# default = true
org.multiverse.api.exceptions.LoadTooOldVersionException.reuse=<type:boolean>
 
# a flag that enables the reuse of the RetryError exception instance
# default = true
org.multiverse.api.exceptions.RetryError.reuse=<type:boolean>
 
# a flag that enables the reuse of the WriteConflictException exception instance
# default = true
org.multiverse.api.exceptions.WriteConflictException.reuse=<type:boolean>
 
Home
close
Loading...
Home Turn Off "Getting Started"
close
Loading...