Critical section

Questions/comments

The shared update problem

Semaphore

Using a semaphore

  Semaphore s;

  public void run () {
      for(int i = 0; i < rounds; i++) {
        s.wait();
        int tmp = counter;
        counter = tmp + 1;
        s.signal();
      }
  }

thread1 thread2

counter = 0

    5. s.wait()
    6. 0 = counter
    7. counter = 1
    8. s.signal()

counter = 1

    5. s.wait()
    6. 1 = counter
    7. counter = 2
    8. s.signal()

counter = 2

    5. s.wait()
    6. 2 = counter
    7. counter = 3
    8. s.signal()

counter = 3

    5. s.wait()
    6. 3 = counter
    7. counter = 4
    8. s.signal()

counter = 4

How to convince yourself that the counter is now correct?

Things to come

Preliminaries

Limited Critical Reference

await statement

Implementing semaphores using await

Formal requirements

Safety/liveness properties

Attempt 1

  int turn = 0; // Start with turn = 0

  // thread 1
     while (true) {
        // Non-critical section
        await (turn == 0) ;
        // Critical Section
        turn = 1;
     }
  // thread 2
     while (true) {
        // Non-critical section
        await (turn == 1) ;
        // Critical Section
        turn = 0;
     }

Showing the mutual exclusion property

Invariants

Attempt 2

Attempt 3

Peterson's algorithm

Showing properties

Remarks

Other low-level primitives

Right for the job?

Beyond busy waiting

Peterson's algorithm in Java

Semaphores in Java

Semaphore mutex = new Semaphore(1);

public void run() {
   try {
      while (true) {
         //Non-critical section
         mutex.acquire();
         //Critical Section
         mutex.release();
         //Non-critical section 
   } catch(InterruptedException e) {}
}

Java built-in locks

Question

If the Java compiler can rearrange the order of statements, can two calls to acquire() be swapped?

Semaphore s1 = new Semaphore(1);
Semaphore s2 = new Semaphore(1);

...
  s1.acquire();
  s2.acquire();
}

Answer: No

From Chapter 17 of the Java Language Specification (17.4.3 and 17.4.4):

Among all the inter-thread actions performed by each thread t, the program order of t is a total order that reflects the order in which these actions would be performed according to the intra-thread semantics of t.

...

Every execution has a synchronization order. A synchronization order is a total order over all of the synchronization actions of an execution. For each thread t, the synchronization order of the synchronization actions (§17.4.2) in t is consistent with the program order (§17.4.3) of t.

Synchronization actions are volatile reads and writes, and locking and unlocking of monitors associated with objects (synchronized). The documentation for Semaphore does not state explicitly that acquire() and release() are synchronization actions. However, the intention clearly is that they are synchronization actions.

Documentation of the Lock interface gives more explicit assurances:

All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock, as described in section 17.4 of The Java™ Language Specification:

  • A successful lock operation has the same memory synchronization effects as a successful Lock action.
  • A successful unlock operation has the same memory synchronization effects as a successful Unlock action. Unsuccessful locking and unlocking operations, and reentrant locking/unlocking operations, do not require any memory synchronization effects.

However, Semaphore does not implement the Lock interface.

What prevents the compiler from reordering the acquire() and release() calls? Let's take a look at the code. The operation that is used under the hood is compareAndSetState (link), which itself executes compareAndSwapInt. The comment for compareAndSetState states 'This operation has memory semantics of a volatile read and write.', which makes it a synchronization action that cannot be reordered.

Counter using Java semaphores

import java.util.concurrent.Semaphore;

class CounterS implements Runnable {

  private Semaphore s = new Semaphore (1);

  private int counter = 0;
  private final int rounds = 100000;

  public void run () {
    try {
      for(int i = 0; i < rounds; i++) {
        try {
          s.acquire();
          counter++;
        } finally {
        s.release();
        }
      }
    } catch (InterruptedException e) {
      System.err.println("Thread interrupted");
      System.exit(-1);
    }
  }

  public static void main (String[] args) {
    try {
      CounterS c = new CounterS ();

      // Create two threads that run our run () method.
      Thread t1 = new Thread (c, "thread1");
      Thread t2 = new Thread (c, "thread2");

      t1.start (); t2.start ();

      // Wait for the threads to finish.
      t1.join (); t2.join ();

      // Print the counter
      System.out.println(c.counter);
    } catch (InterruptedException e) {
      System.out.println ("Main thread interrupted!");
      System.exit(-1);
    }
  }
}

Summary



Concurrent Programming 2016 - Chalmers University of Technology & Gothenburg University