STM and Structured Concurrency

The Battle with Multithreading Dragons

Bartek „Koziołek” Kuczyński

Człowiek od robienia hmhmm.

 

https://koziolekweb.pl/001-o-mnie.html

Dorrik Hexhammer

Dwarf warrior-engineer

Lawful Neutral Chaos

 

 

 

Alignment

Immutability

Atomics

Eventual Consistency

Lock

synchronized
Thread.sleep
Thread.yeld

Single Thread

Possible consistency

Good

Neutral

Evil

Motivation

Our Data protection

  • We protect our code
  • We know why we do that
  • We can trace changes of code AND data
  • White Box

Clients and Services

  • We need to protect external code (lib or service)
  • We read a doc ;)
  • We don't know why we need to do that
  • Black Box (probably mimic)

Processes

  • Long running, multistep processes
  • Some elements could be run in parallel
  • We need to coordinate a whole process
  • Grey Box

Single Object Synchronization

class Account {

	private int balance = 0;

	public synchronized void withdraw() {balance -= 1;}

	public synchronized void deposit() {balance += 1;}
	//…

}
class Account {

	private final Object monitor = new Object();
	private int balance = 0;

	public void withdraw() {
		synchronized (monitor) {
			balance -= 1;
		}
	}

	public void deposit() {
		synchronized (monitor) {
			balance += 1;
		}
	}
}

Quick reminder

synchronized(object){} -> object is monitor

synchronized method() -> synchronized(this){}

synchronized static method() -> synchronized(this.getClass()){}

Many Objects Synchronization

interface BankService {
	void transfer(Account from, Account to, int value);
}
//…
public class SimpleAccount implements Account {

	private int balance;

	@Override
	public void deposit(int depo) {
		//…
		balance += depo;
	}

	@Override
	public void withdraw(int with) {
		//…
		balance -= with;
	}
}
public void theProblem() {
	var bob = new Account("Bob", RUNS);
	var alice = new Account("Alice", RUNS);

	var aliceToBob = prepare(RUNS, 
    	() -> bankService.transfer(alice, bob, 1));
	var bobToAlice = prepare(RUNS, 
    	() -> bankService.transfer(bob, alice, 1));

	var aliceThread = new Thread(aliceToBob, "Alice");
	var bobThread = new Thread(bobToAlice, "Bob");
	aliceThread.start();
	bobThread.start();
	aliceThread.join();
	bobThread.join();
	System.out.printf(STR."""
    	alice = \{alice} 
        bob = \{bob}""");
}

BankService implementation

method level synchronized

class MethodSync implements BankService {

	public synchronized void transfer(Account from, Account to, int value) {
		from.withdraw(value);
		to.deposit(value);
	}
}

BankService implementation

args level synchronized

class ArgumentSync implements BankService {

	public void transfer(Account from, Account to, int value) {
		synchronized (from) {
			synchronized (to) {
				from.withdraw(value);
				to.deposit(value);
			}
		}
	}
}

BankService implementation

ordered args level synchronized

class OrderedArgumentSync implements BankService {
	public void transfer(Account from, Account to, int value, Comparator<Account> comparator) {
		final int compare = comparator.compare(from, to);
		if (compare > 0) {
			synchronized (from) {
				synchronized (to) {
					from.withdraw(value);
					to.deposit(value);
				}
			}
		} else {
			synchronized (to) {
				synchronized (from) {
					from.withdraw(value);
					to.deposit(value);
				}
			}
		}
	}
}

BankService implementation

generic ordered args level synchronized

class GenericOrderedSync {
	public <S1, S2, R> R perform(S1 s1, S2 s2, BiFunction<S1, S2, R> todo,
    							BiFunction<S1, S2, Integer> orderJudge) {
		final Integer order = orderJudge.apply(s1, s2);
		if (order > 0) {
			synchronized (s1) {
				synchronized (s2) {
					return todo.apply(s1, s2);
				}
			}
		} else {
			synchronized (s2) {
				synchronized (s1) {
					return todo.apply(s1, s2);
				}
			}
		}
	}
}

Let's try another path in this dungeon

Dorrik new skill

The Aspect

The Aspect-Oriented Programming

Let's define annotation

@Target(METHOD)
@Retention(RUNTIME)
@Documented
public @interface ExtendedLock {
}

And pointcut

@Aspect
public class ExtendedLockAspect {
	private final ExtendedLockHandler lockHandler = new ExtendedLockHandler();
    
	@Around("@annotation(pl.koziolekweb.extendedlock.ExtendedLock)")
	public Object callWithLock(ProceedingJoinPoint tjp) throws Throwable {
		var args = tjp.getArgs();
		try {
			while (!lockHandler.lock(args)) {			}
			final Object proceed = tjp.proceed();
			while (!lockHandler.unlock(args)) {}
			return proceed;
		} catch (Throwable t) {
			while (!lockHandler.unlock(args)) {} throw t;
		} finally {
			while (!lockHandler.unlock(args)) {}
		}
	}
}

And handler

public boolean lock(Object[] elements) {
	if (lock.tryLock()) {
		clean();
		for (Object element : elements) {
			if (lockMap.containsKey(element)
				&& (!lockMap.get(element).isSameThread(getCurrentThread()))) {
					free();
					return false;
			}
		}
		for (Object element : elements) {
			if (lockMap.containsKey(element))
				lockMap.get(element).inc();
			else
				lockMap.put(element, new LockCounter(getCurrentThread()));
		}
		free();
		return true;
	}
	return false;
}

Is it work?

To kill a Dragon, you need?

Sword

To make a sword, you need?

Iron

To make an iron, you need?

Cave

To make a cave, you need?

Mountain

To make a mountain, you need?

Kingdom

To make a kingdom, you need?

King

To make a king, you need?

Kill a dragon!

If only we have a spell…

The Database

Atomic

Consistency

Isolation

Durability

Dorrik new skill

The STM

Software Transactional Memory

STM Account

public class StmAccount implements Account {
	private Ref balance;

	@SneakyThrows
	public void deposit(final int depo) {
		LockingTransaction.runInTransaction(() -> {
			balance.set((Integer) (balance.deref()) + depo);
			return true;
		});
	}
	@SneakyThrows
	public void withdraw(int with) {
		LockingTransaction.runInTransaction(() -> {
			balance.set((Integer) (balance.deref()) - with);
			return true;
		});
	}
}

BankService implementation

not synchronized

class MethodSync implements BankService {

	public void transfer(Account from, Account to, int value) {
		from.withdraw(value);
		to.deposit(value);
	}
}

STM Account

public class ExternalStmAccount implements Account {
	private Ref balance;

	@SneakyThrows
	public void deposit(final int depo) {
		balance.set((Integer) (balance.deref()) + depo);
	}

	@SneakyThrows
	public void withdraw(int with) {
		balance.set((Integer) (balance.deref()) - with);
	}
}

BankService implementation

STM support

class StmSync implements BankService {

	@SneakyThrows
	public void transfer(Account from, Account to, int value) {
		LockingTransaction.runInTransaction(() -> {
			from.withdraw(value);
			to.deposit(value);
			return true;
		});
	}
}

Time to new adventure!

And more xp

Process Dragon

Process Dragon

  • Has head
    • Semaphore
  • Has tail
    • CountDownLatch
    • CyclicBarrier

We have Phasers!

We have Phasers!

public class PhaserExample {

	public static void main(String[] args) {
		Phaser phaser = new Phaser(1);
		new Thread(new Action("T1", phaser), "T1").start();
		new Thread(new Action("T2", phaser), "T2").start();
		phaser.arriveAndAwaitAdvance();
		new Thread(new Action("T3", phaser), "T3").start();
		new Thread(new Action("T4", phaser), "T4").start();
		phaser.arriveAndDeregister();
	}
}

We have Phasers!

class Action implements Runnable {
	private final String threadName;
	private final Phaser phaser;

	public void run() {
		phaser.arriveAndAwaitAdvance();
		try {
			System.out.println("start " + threadName);
			Thread.sleep(1000);
			if (Math.random() > 0.2) {
				throw new IllegalStateException("Die!");
			}
			System.out.println("end " + threadName);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		phaser.arriveAndDeregister();
	}
}

We have Phasers!

start T2
start T1
end T1
end T2
start T4
start T3
Exception in thread "T4" Exception in thread "T3" 
java.lang.IllegalStateException: Die!
	at pl.koziolekweb.simple.Action.run(PhaserExample.java:35)
	at java.base/java.lang.Thread.run(Thread.java:1583)
java.lang.IllegalStateException: Die!
	at pl.koziolekweb.simple.Action.run(PhaserExample.java:35)
	at java.base/java.lang.Thread.run(Thread.java:1583)

Dorrik new skill

Structured

Concurrency

What to do?

class SimpleTask implements Callable<Void> {
	
	public Void call() {
		System.out.println("start " + threadName);
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		if (Math.random() > 0.2) {
			throw new IllegalStateException("Die!");
		}
		System.out.println("end " + threadName);
		return null;
	}
}

Back to fantasy word!

public class StructuredConcurrenceExample {

	public static void main(String[] args) throws InterruptedException,
    											ExecutionException {
		try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
			StructuredTaskScope.Subtask<Void> t1 = scope.fork(new SimpleTask("T1"));
			StructuredTaskScope.Subtask<Void> t2 = scope.fork(new SimpleTask("T2"));
			scope.join().throwIfFailed();

			StructuredTaskScope.Subtask<Void> t3 = scope.fork(new SimpleTask("T3"));
			StructuredTaskScope.Subtask<Void> t4 = scope.fork(new SimpleTask("T4"));
			scope.join().throwIfFailed();
		}
	}
}

You are in a bad dungeon!

We need custom scope!

Custom scope

class Sandbox<T> extends StructuredTaskScope<T> {
	@Override
	protected void handleComplete(Subtask<? extends T> subtask) {
		if (subtask.state() == Subtask.State.FAILED) {
			System.out.println(STR."""
				end \{((SimpleTask) subtask.task()).getThreadName()}
				but \{subtask.exception().getMessage()}""");
		}
	}

	@Override
	public Sandbox<T> join() throws InterruptedException {
		super.join();
		return this;
	}
}

Custom scope

public class CustomScopeStructuredConcurrency {

	public static void main(String[] args) throws InterruptedException {
		try (var scope = new Sandbox<Void>()) {
			StructuredTaskScope.Subtask<Void> t1 = scope.fork(new SimpleTask("T1"));
			StructuredTaskScope.Subtask<Void> t2 = scope.fork(new SimpleTask("T2"));
			scope.join();
            
			StructuredTaskScope.Subtask<Void> t3 = scope.fork(new SimpleTask("T3"));
			StructuredTaskScope.Subtask<Void> t4 = scope.fork(new SimpleTask("T4"));
			scope.join();
		}
	}
}

Custom scope

start T2
start T1
end T2
end T1
but Die!
start T3
start T4
end T3
end T4