Lecture 21 Concurrency Control

Joseph Haugh

University of New Mexico

Concurrency Control Definition

  • Layering of synchronization and control policies over base mechanisms
  • Key assumption: ground classes have been designed to be amenable to the desired forms of control
  • Strategies
    • adding policy control in subclasses
    • controlling delegated actions
    • representing messages as objects

Adding Synchronization Via Subclassing

  • Ground level class
    • provides non-public methods
    • enforces no invariants
  • Subclass
    • implements the synchronization policy, e.g.,
      • atomic updates
      • bounded counter
    • delegates actions to ground methods

Bounded Counter Code

public class GroundCounter {
  protected long count;

  protected GroundCounter(long c) {
    count = c;
  }

  protected long value() { return count; }

  protected void inc() {
    ++count;
  }

  protected void dec() {
    --count;
  }
}

Bounded Counter Code

public class BoundedCounter extends GroundCounter {
  private final long MIN, MAX;

  public BoundedCounter(final long MIN, final long MAX) {
    super(MIN);
    this.MIN = MIN;
    this.MAX = MAX;
  }

  public synchronized long value() { return super.value(); }

  public synchronized void inc() {
    while (value() >= MAX) {
      try { wait(); } catch(InterruptedException e) {};
    }
    super.inc();
    notifyAll();
  }

  public synchronized void dec() {
    while (value() <= MIN) {
      try { wait(); } catch(InterruptedException e) {};
    }
    super.dec();
    notifyAll();
  }
}

Anomalies to be Avoided

If all relevant variables and methods are declared protected the subclass can generally implement the desired policy

  • Frequent problems
    • failing to track all conditions on which the subclass depends
    • differences in state representation
    • immutable variable in the superclass is being modified
    • introducing waits for which the subclass has no matching notifications
    • extensions which require a transition from notify to notifyAll

Layering Guards

  • Ground level class
    • ensures atomicity
    • generates exception on empty stack
    • forces a busy wait solution in a concurrent setting
  • Subclass
    • eliminates the exception generation
    • adopts a wait/notify protocol
    • delegates select actions to ground methods

Waiting Stack Code

public class Stack {
  public synchronized boolean isEmpty() { 
    // ...
  }
  public synchronized void push(Object x) { 
    // ...
  }
  public synchronized Object pop() throws StackEmptyException {

    if (isEmpty()) { 
      throw new StackEmptyException();
    } else { 
      // ...
    }
  }
}

Waiting Stack Code

public class WaitingStack extends Stack {
  public synchronized void push(Object x) {
    super.push(x);
    notifyAll();
  }

  public synchronized Object pop() throws StackEmptyException {
    while(isEmpty()) {
      try { 
        wait(); 
      } catch (InterruptedException e) {}
    }
    return super.pop();
  }
}

Conflict Sets and Others

  • Design paradigm
    • track the superclass state through the introduction of auxiliary variables
    • block method calls based on the current abstract state of the superclass
  • Sample formalizations
    • conflict set
    • conflict graph
    • finite set control

Conflict Set

A conflict set is a list of pairs of operations that cannot run concurrently

  • Each pair (m1, m2) means: if m1 is executing, m2 must wait (and vice versa)
  • If a pair is absent, the two operations may run simultaneously
  • Example: Inventory
    • (store, retrieve): a store and a retrieve cannot overlap
    • (retrieve, retrieve): two retrieves cannot overlap

Conflict Graph

A conflict graph is a visual equivalent of the conflict set

  • Each node is an operation
  • A double-headed arrow between two nodes means they conflict
  • If no arrow exists between two nodes, they may run concurrently

The conflict graph makes it easy to see at a glance which operations are mutually exclusive and which can be parallelized

Finite State Control

Finite state control describes how the policy is enforced at runtime using a state machine

  • Each state represents the current abstract state of the object (e.g., how many readers or writers are active)
  • Arrows show transitions triggered by operation entry and return
  • Entry arrows (left side): a thread requests to begin an operation
    • it may block until the state allows it
  • Return arrows (right side): a thread finishes and updates state, potentially unblocking waiters

This maps directly to the synchronized blocks that bracket each operation in the implementation

Illustration: Inventory Control

Illustration: Inventory Code

public class Inventory extends GroundInventory {
  protected int storing = 0;
  protected int retrieving = 0;

  public void store(String desc, Object item,
                    String supplier) {
    synchronized (this) {
      while (retrieving != 0) {
        try { wait(); } catch (InterruptedException e) {}
      }
      ++storing;
    }

    super.store(desc, item, supplier);

    synchronized (this) {
      if (--storing == 0) {
        notifyAll();
      }
    }
  }

Illustration: Inventory Code

  public void retrieve(String desc, Object item,
                       String supplier) {
    synchronized (this) {
      while (storing != 0 || retrieving != 0) {
        try { wait(); } catch (InterruptedException e) {}
      }
      ++retrieving;
    }

    super.retrieve(desc, item, supplier);

    synchronized (this) {
      if (--retrieving == 0) {
        notifyAll();
      }
    }
  }

Readers and Writers: A Common Design Pattern

  • A simple conflict graph
  • Readily ensured safety
  • Major variations when it comes to liveness and lack of starvation

General Pattern

  • Track the number of waiting and active readers and writers
  • Bracket the read/write operations with before/after control code
  • Same design accommodates a wide range of policies

General Pattern Code

public abstract class ReadWrite {
  protected int activeReaders = 0;
  protected int activeWriters = 0;
  protected int waitingReaders = 0;
  protected int waitingWriters = 0;

  protected abstract void doRead();
  protected abstract void doWrite();

  public void read() {
    beforeRead();
    doRead();
    afterRead();
  }

  public void write() {
    beforeWrite();
    doWrite();
    afterWrite();
  }

Control Code: before/afterRead

protected synchronized void beforeRead() {
  ++waitingReaders;
  while (!allowReader()) {
    try { wait(); } catch (InterruptedException e) {}
  }
  --waitingReaders;
  ++activeReaders;
}

protected synchronized void afterRead() {
  --activeReaders;
  notifyAll();
}

Control Code: before/afterWrite

protected synchronized void beforeWrite() {
  ++waitingWriters;
  while (!allowWriter()) {
    try { wait(); } catch (InterruptedException e) {}
  }
  --waitingWriters;
  ++activeWriters;
}

protected synchronized void afterWrite() {
  --activeWriters;
  notifyAll();
}

Policy: Reading Priority

  • Direct enforcement of the conflict set rules
    • readers can read unless a writer is writing
    • a writer can start writing if no other thread is reading or writing
  • Writers can be starved if readers continue to use the resource
protected boolean allowReader() {
  return activeWriters == 0;
}

protected boolean allowWriter() {
  return activeReaders == 0 && activeWriters == 0;
}

Policy: Reading Preemption

  • Writer starvation is prevented by blocking new reading threads once a writer is waiting
  • Readers can be kept out by multiple writers waiting in line
protected boolean allowReader() {
  return waitingWriters == 0 && activeWriters == 0;
}

protected boolean allowWriter() {
  return activeReaders == 0 && activeWriters == 0;
}

Policy: Turn Taking

  • If both readers and writers are waiting, alternate between readers and writers
  • Still, there is no guarantee that no readers or writers are blocked forever

Policy: Turn Taking

private boolean canWrite;

public synchronized void afterRead() {
  canWrite = true; // ...previous afterRead code...
}
public synchronized void afterWrite() {
  canWrite = false; // ...previous afterWrite code...
}

protected boolean allowReader() {
  if (waitingWriters > 0 && waitingReaders > 0) {
    return !canWrite && activeWriters == 0;
  }
  return waitingWriters == 0 && activeWriters == 0;
}

protected boolean allowWriter() {
  if (waitingWriters > 0 && waitingReaders > 0) {
    return canWrite && activeWriters == 0;
  }
  return activeReaders == 0 && activeWriters == 0;
}

Policy: Custom Scheduler

  • Each reader/writer makes a request and gets a ticket number
  • Each request and its type (read/write) is placed in a queue according with some policy that optimizes throughput and ensures fairness
  • A writer starts working when its ticket is first in the queue
  • A reader starts working when its ticket is part of a sequence of read requests at the front of the queue
  • Tickets are returned upon completion of the operation
  • Ticket counter is reset when no tickets are out

Basic Adapter Concept

  • An adapter is a specialized wrapper that offers a new interface based on an existing class
  • The main advantages are
    • flexibility
      • a range of services based on an existing class
      • dynamic changes of the base class in use
    • reduced coding effort
    • reuse of legacy code
  • Adapters do not have access to protected fields and methods
    • this is distinct from subclassing

Example: Adapter

Suppose a legacy class measures temperature in Fahrenheit:

public class FSensor {
  public double getTemp() { 
    /* returns degrees F */ 
  }
}

An adapter wraps it to provide a Celsius interface:

public class CSensor {
  private FSensor sensor;
  public CSensor(FSensor sensor) {
    this.sensor = sensor;
  }
  public double getTemperature() {
    return (sensor.getTemp() - 32) * 5.0 / 9.0;
  }
}
  • CelsiusSensor has no access to FahrenheitSensor’s internals
  • The client only sees getTemperature() in Celsius
  • The legacy class is reused without modification

Example: Rocket

  • Internal state
    • location (point in 3D space, meters)
    • velocity (3D vector, meters/sec)
    • flight time (seconds)
  • Methods
    • read/update location
    • read/update velocity
    • increment flight time

Delegation

The interface is the same as that of the ground class

  • actions are simply redirected
  • returned values are passed up
  • What is the gain?
    • flexibility

Refactoring

Different methods are provided and coded in terms of the ground class

public void reset() {
  setFlightTime(0);
  setLocation(new PosPoint());
  setVelocity(new VelVec());
}

public void step(VelVec vel) {
  flightTime++;
  velocity = vel;
  location.add(velocity);
}

Superposition

Superposition: a process by which variables are introduced to monitor the state of the ground class

  • to extend functionality without actually affecting the state
  • to facilitate reasoning about the computation
public void step(VelVec vel) {
  flightTime++;
  velocity = vel;
  location.add(velocity);
  if(velocity.compare(MAX_SAFE_VEL) > 0) {
    unsafeCount++;
  }
}

Proxy: A Useful Design Pattern

  • Proxy: an object that stands in place of another
    • displays an appropriate veneer
    • delegates all actions
  • The original object may be a
    • concrete object: present in the system and having the same interface
    • abstract object: the result of refactoring

Illustration: Proxy Examples

Synchronized Adapters

  • In the presence of concurrency, objects designed to function in a single threaded environment need to be protected
  • The wrapper can provide the needed synchronization (when the ground object is private)

Synchronized Adapters with Access Control

  • An adapter can also introduce blocking of threads in a way that is sensitive to the state of the ground object
  • Illustration: block thread waiting for a specific target velocity to be reached and release it when
    • a reset has been issued (return false)
    • the target velocity has been reached or exceeded (return true)

Synchronized Adapters with Access Control

public synchronized void step(VelVec vel) {
  // ...
  notifyAll();
}

public synchronized void reset() {
  // ...
  notifyAll();
}

public synchronized boolean targetVelocity(VelVec vel) {
  // mag returns integer magnitude of vector
  while (mag(velocity) != 0 && mag(velocity) < mag(vel)) {
    try { wait(); } catch (InterruptedException e) { }
  }
  if(mag(velocity) == 0) return false; // reset issued
  else return true; // target velocity reached
}

What’s Wrong?

notifyAll wakes waiting threads, but they do not resume immediately, each must re-acquire the lock before continuing

  • Other synchronized methods can acquire the lock first
  • By the time the waiting thread resumes, shared state may have changed

What’s Wrong?

  • The race:
    1. reset sets velocity to 0 and calls notifyAll
    2. Before targetVelocity can re-acquire the lock, step runs and sets velocity to a nonzero value
    3. targetVelocity finally resumes and checks mag(velocity) == 0, but it is no longer 0, so it incorrectly returns true
  • The bug:
    • checking velocity after waking up does not reliably tell you why you woke up

Synchronized Adapters with Access Control

public synchronized void step(VelVec vel) {
  // ...
  notifyAll();
}

public synchronized void reset() {
  // ...
  tracking = false;
  notifyAll();
}

public synchronized boolean targetVelocity(VelVec vel) {
  tracking = true; // remains true unless reset
  while (tracking && mag(velocity) < mag(vel)) {
    try { wait(); } catch (InterruptedException e) { }
  }
  if(!tracking) return false;
  tracking = false;
  return true;
}

Extending Atomicity

  • An adapter can augment existing methods with additional functionality while making the entire operation atomic
  • Illustration: augment the step method to include a logging action
public synchronized void step(VelVec vel) {
  flightTime++;
  velocity = vel;
  location.add(velocity);
  Log.addEntry(flightTime, location);
}

class Log {
  public static synchronized void addEntry(int time,
                                           PosPoint loc) {
    // store/print entry...
  }
}

Read Only Adapters

  • It is often the case that data needs to be protected against unauthorized modification
  • A read-only wrapper gives full access to the object data without the risk of being modified

Read Only Adapters

Programming Concerns

  • The methods of the immutable object should be declared final
  • The base object B should not be leaked to users of the immutable object X