Concurrent Programming

The mutual exclusion Problem

The Root of The Problem

  • Non-determinism caused by concurrent threads accessing shared mutable state.
  • It helps to encapsulate state in actors or transactions, but the fundamental problem stays the same.

Non-determinism = parallel processing + mutable state.

  • To get deterministic processing, avoid the mutable state!
  • Avoiding mutable state means programming functionally.

Race Condition

A race condition occurs when two or more operations must execute in a specific sequence.

But the system's substantive behavior is not guaranteed.

Leading to erratic and unpredictable results.

Example in Java

public class Counter {
    int value = 0;

    void increment() {
        int localCounter = value;
        System.out.println(threadName() + " reads counter as: " + localCounter);

        localCounter = localCounter + 1;
        
        value = localCounter;
        System.out.println(threadName() + " updated counter to: " + value);
    }
}

Run

public class Main {
    public static void main(String[] args) {
        var counter = new Counter();

        // Two threads trying to increment the counter simultaneously
        Thread t1 = new Thread(counter::increment, "Thread 1");
        Thread t2 = new Thread(counter::increment, "Thread 2");

        startAll(t1, t2);
        joinAll(t1, t2);

        System.out.println("Expected value: 2, actual value: " + counter.value);
    }
}
Thread 1 reads counter as: 0
Thread 2 reads counter as: 0
Thread 1 updated counter to: 1
Thread 2 updated counter to: 1
Expected value: 2, actual value: 1

Data Race Diagram

Solution: Enforce Single Access

If we enforce a rule that only one process may enter the method increment at a time then:

  • Thread1 enters increment first and creates a lock
  • Thread2 tries to enter but is blocked
  • Thread1 completes execution and releases the lock
  • Thread2 resumes and executes increment
public class Counter {
    int value = 0;

    synchronized void increment() {
        int localCounter = value;
        System.out.println(threadName() + " reads counter as: " + localCounter);

        localCounter = localCounter + 1;
        
        value = localCounter;
        System.out.println(threadName() + " updated counter to: " + value);
    }
}

Memory Sharing between threads

Something is rotten in the state of Denmark

boolean a = false, b = false;
int x = -1, y = -1;
    
    
void test(int execution) {
    var t1 = new Thread(() -> {
        sleep(1);
        a = true;
        y = b ? 0 : 1;
    });
    var t2 = new Thread(() -> {
        sleep(1);
        b = true;
        x = a ? 0 : 1;
    });
    startAll(t1, t2);
    joinAll(t1, t2);

    if (x == 1 && y == 1)
        throw new RuntimeException("Failed at execution number : " + execution);
}

Alternatives

  • First Alternative: t1 runs first
    • t1 writes true to a then reads b and sees false –> writes 1 to y
    • t2 writes true to b then reads a and sees true –> writes 0 to x
    • x == 0 && y == 1
  • Second Alternative: t2 runs first.
    • x == 1 && y = 0
  • Third Alternative: Partial run
    • t1 writes true to a and is interrupted.
    • t2 writes true to b
    • t1 and t2 both see true so they write 0 to x and y
    • x == 0 && y == 0
  • Conclusion: there is no execution in which x = 1 and y = 1

Let's try it!

public static void main(String[] args) {
	for (var i = 0; i < 10_000; i++)
    	new Test().test(i);
}
Exception in thread "main" java.lang.RuntimeException: Failed at execution number : 880
	at org.example.counter.Test.test(Test.java:25)
	at org.example.counter.Test.main(Test.java:30)

Why ?

  • Threads can run in different cores using there own memory (For example L1 cache)
  • Threads are synchronized trough shared memory. For example after they are joined.
  • Then this is possible:
    • t1 writes true to a then reads b and sees false –> writes 1 to y
    • t2 writes true to b then reads a and sees false -> writes 1 to x
  • Because t2 have their own value of a that has not yet been synchronized

A posible solution in Java

Declare a and b volatile

volatile boolean a, b;

This forces the JVM to synchronize the variable to and from shared memory

Volatile is lightweight and faster than locking

Competition among Processes for Resources

Three main control problems:

  • Need for Mutual Exclusion. Only on process at a time be allowed in the critical section.
  • Deadlock
  • Starvation

Deadlock

Deadlock can be defined as the permanent blocking of a set of processes that compete for resources.
A set of processes is deadlocked when each process in the set is blocked
waiting for a resource that is hold by another blocked process in the set.

Deadlock Diagram

Example in Java

public void transferTo(BankAccountSync to, double amount) {
  synchronized (this) {
	  // Lock over 'this' acquired
	
	  // Try to lock 'to'
	  synchronized (to) {
		// Lock over 't0' acquired.
		withdraw(amount);
		to.deposit(amount);
	  }
  	  // lock over 'to' released
  }
  // lock over 'this' released
}
  • Show Demo

Starvation

A situation where a process is perpetually denied the resources it needs to progress,
while other processes are favored,
leading to indefinite waiting.

Starvation

Critical Session Implementations

  • Disable Interruptions
  • Busy Loop
  • Special Machine Instructions
    • testAndSet
    • compareAndSwap

Disable Interruptions

public class Counter {
    int value = 0;

    void increment() {
        disableInterruptions();
        
        int localCounter = value;
        localCounter = localCounter + 1;
        value = localCounter;

        enableInterruptions();
    }
}
  • Only work in uni-processors
  • Usually inefficient

Basic implementation


boolean lock = false;

void increment() {
	while (lock) {
		// Busy Loop
	}
	lock = true;
	// Begin Critical Section
		int localCounter = value;
		System.out.println(threadName() + " reads counter as: " + localCounter);
	
		localCounter = localCounter + 1;
		value = localCounter;
	// End Critical Section
	lock = false;
	System.out.println(threadName() + " updated counter to: " + value);
}

Needs Volatile!

volatile boolean lock = false; 

To ensure that when one threads modify lock the others see it

But It has a problem

while (lock) {
	// Busy Loop
}
// I can have a Context switch here !
lock = true;

There is Hardware Support to do this right

For example, Compare and Set instruction

void increment() {
	while (v.compareAndSet(false, true)) { 
		// busyLoop
	}
    
	// Critical section
	// ...
}

Atomically test and modify the value on sucess

Hardware support

  • The good:
    • Applicable to any number of processes.
  • The bad:
    • Busy-waiting consumes processor time.
  • Solution:
    • After some spinning return control to the operating system.
    • while (lock.compareAndSet(false, true)) Thread.yield();

Disclaimer

This is to show How it can be implemented

You should use the libraries provided by the language/framework

What if I want to do it (the bad way) in Rust???

fn counter() -> i32 {
    let mut counter = 0;
    thread::scope(|s| {
        s.spawn(|| counter += 1);
        s.spawn(|| counter += 1);
    });
    return counter
}
error[E0499]: cannot borrow `counter` as mutable more than once at a time

... Etc..

The borrow checker protects me from data races!