Wednesday 29 February 2012

Standard Library (2 of 5). Multitasking

Multitasking is implemented in the Libretto standard library. The basic components of concurrent computing in Libretto are the actor model and software transactional memory based on the optimistic approach.


Actor Model

In Libretto the actor model is basically adopted from Erlang. It is based on the concept of an actor – an active object, which asynchronously interacts with other actors. Actors communicate via messages. Actor is a underdetermined class including methods

  • act, which defines the behavior of the actor, and
  • start, which launches the actor.

Any Libretto program is executed in a system environment consisting of several actors. These actors are launched, when the program starts. The method process returns the actor, in which the current code is executed.

As an example, let us consider a ‘sleepy sort’, a funny sequence ordering algorithm, which is defined as follows. For each integer n from the sequence to be sorted a separate actor is launched, which gets n and then falls asleep for n secs. When it wakes up, the actor sends n back to the dispatcher. The dispatcher forms the sequence of the integers in the order it receives them.
class SleepyActor(fix n: Int, fix dispatcher: Actor) extends Actor {
  def act {
    wait(n * 1000)
    dispatcher ! n
  }
}

def Int* sleepSort {
  var ordered
  var count = length
  this ?[$ >= 0] ?(error("negative!")). SleepyActor($, process). start
  
  loop receive {
    case n if Int => 
      ordered += n
      count = count - 1
      if (count == 0) return ordered
  }
}

(3,1,2,4).sleepSort  //  1 2 3 4
An actor here is an instance of the class SleepyActor, which:

  1. receives integer n, and the dispatcher actor (the function sleepSort);
  2. falls asleep for n secs;
  3. on awake, sends n back to the dispatcher actor.

The function wait suspends execution for the given number of millisecs. The call dispatcher!n, sends n back to the dispatcher. The behavior of an actor is determined by the method act. The method start in the dispatcher creates a new thread and starts the actor in it. Having launched all sleepy actors, the dispatcher starts a loop, in which handles incoming numbers and counts them. The loop stops as soon as the last integer arrives.

Transactions

The concurrency control mechanism in Libretto is based on software transactional memory (STM). A transaction is executed within an atomic block
atomic {
  transaction body
}
which is seen from outside as single and indivisible. If a problem occurs (e.g. some resource suffers from simultaneous writes), then the transaction is rolled back to its initial state (all fields and variables marked by ref restore their initial values). The transaction is relaunched as soon as one of the fields or variables marked by ref has changed its value. The programmer can relaunch the transaction ‘manually’ by using the method retry. While a transaction is being executed, the changed values of ref variables and ref fields are not accessible from outside. And only after the successful completion of the transaction, the changes are committed and become accessible globally.

As an example, let us consider the customer-account management in a bank. First, introduce the class of financial transactions (please, do not confuse them with STM transactions):
class TransType
object Income extends TransType
object Expenditure extends TransType

fix class Transaction(type: TransType, amount: Int, date: Date)
Now let us define the class Account with two methods deposit and withdraw. Each of these method definitions contains a block atomic {...}, which ensures the correctness of account transactions. In particular, it is impossible to withdraw money if its amount is insufficient on the account. If an account operation can not be fulfilled, its execution is suspended until the operation conditions are met. Several financial transactions can be made concurrently (in concurrent actors).
class Account(fix owner: Int) {
  ref var amount: Int = 0
  ref var transactions: Transaction* // transactions history
  
  def withdraw(wamount: Int) {
    atomic {
      if (amount < wamount) retry // money not enough
      amount = amount - wamount
      transactions += Transaction(Expenditure, wamount, date)
    }
  }

  def deposit(damount: Int) {
    atomic {
      amount = amount + damount
      transactions += Transaction(Income, damount, date)
    }
  }
}
The predefined function retry stops the current execution of its atomic block, restores the initial state of variables and fields, and suspends the transaction until one of ref-variables or ref-fields change its value. In particular, the execution of the method withdraw is suspended until the moment when the account has enough money.

The methods error and jump (see section Exception Handling) behave differently in atomic blocks. If an exception is thrown by error, and it can not be caught within this block, then the transaction is rolled back. By leaping out of the atomic block with jump we say that its execution is completed successfully and the results of the transaction should be committed.

No comments:

Post a Comment