/** * Copyright (c) Rich Hickey. All rights reserved. * The use and distribution terms for this software are covered by the * Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) * which can be found in the file epl-v10.html at the root of this distribution. * By using this software in any fashion, you are agreeing to be bound by * the terms of this license. * You must not remove this notice, or any other, from this software. **/ /* rich Jul 26, 2007 */ package clojure.lang; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.CountDownLatch; @SuppressWarnings({"SynchronizeOnNonFinalField"}) public class LockingTransaction{ public static final int RETRY_LIMIT = 10000; public static final int LOCK_WAIT_MSECS = 100; public static final long BARGE_WAIT_NANOS = 10 * 1000000; //public static int COMMUTE_RETRY_LIMIT = 10; static final int RUNNING = 0; static final int COMMITTING = 1; static final int RETRY = 2; static final int KILLED = 3; static final int COMMITTED = 4; final static ThreadLocal<LockingTransaction> transaction = new ThreadLocal<LockingTransaction>(); static class RetryEx extends Error{ } static class AbortException extends Exception{ } public static class Info{ final AtomicInteger status; final long startPoint; final CountDownLatch latch; public Info(int status, long startPoint){ this.status = new AtomicInteger(status); this.startPoint = startPoint; this.latch = new CountDownLatch(1); } public boolean running(){ int s = status.get(); return s == RUNNING || s == COMMITTING; } } static class CFn{ final IFn fn; final ISeq args; public CFn(IFn fn, ISeq args){ this.fn = fn; this.args = args; } } //total order on transactions //transactions will consume a point for init, for each retry, and on commit if writing final private static AtomicLong lastPoint = new AtomicLong(); void getReadPoint(){ readPoint = lastPoint.incrementAndGet(); } long getCommitPoint(){ return lastPoint.incrementAndGet(); } void stop(int status){ if(info != null) { synchronized(info) { info.status.set(status); info.latch.countDown(); } info = null; vals.clear(); sets.clear(); commutes.clear(); //actions.clear(); } } Info info; long readPoint; long startPoint; long startTime; final RetryEx retryex = new RetryEx(); final ArrayList<Agent.Action> actions = new ArrayList<Agent.Action>(); final HashMap<Ref, Object> vals = new HashMap<Ref, Object>(); final HashSet<Ref> sets = new HashSet<Ref>(); final TreeMap<Ref, ArrayList<CFn>> commutes = new TreeMap<Ref, ArrayList<CFn>>(); final HashSet<Ref> ensures = new HashSet<Ref>(); //all hold readLock void tryWriteLock(Ref ref){ try { if(!ref.lock.writeLock().tryLock(LOCK_WAIT_MSECS, TimeUnit.MILLISECONDS)) throw retryex; } catch(InterruptedException e) { throw retryex; } } //returns the most recent val Object lock(Ref ref){ //can't upgrade readLock, so release it releaseIfEnsured(ref); boolean unlocked = true; try { tryWriteLock(ref); unlocked = false; if(ref.tvals != null && ref.tvals.point > readPoint) throw retryex; Info refinfo = ref.tinfo; //write lock conflict if(refinfo != null && refinfo != info && refinfo.running()) { if(!barge(refinfo)) { ref.lock.writeLock().unlock(); unlocked = true; return blockAndBail(refinfo); } } ref.tinfo = info; return ref.tvals == null ? null : ref.tvals.val; } finally { if(!unlocked) ref.lock.writeLock().unlock(); } } private Object blockAndBail(Info refinfo){ //stop prior to blocking stop(RETRY); try { refinfo.latch.await(LOCK_WAIT_MSECS, TimeUnit.MILLISECONDS); } catch(InterruptedException e) { //ignore } throw retryex; } private void releaseIfEnsured(Ref ref){ if(ensures.contains(ref)) { ensures.remove(ref); ref.lock.readLock().unlock(); } } void abort() throws AbortException{ stop(KILLED); throw new AbortException(); } private boolean bargeTimeElapsed(){ return System.nanoTime() - startTime > BARGE_WAIT_NANOS; } private boolean barge(Info refinfo){ boolean barged = false; //if this transaction is older // try to abort the other if(bargeTimeElapsed() && startPoint < refinfo.startPoint) { barged = refinfo.status.compareAndSet(RUNNING, KILLED); if(barged) refinfo.latch.countDown(); } return barged; } static LockingTransaction getEx(){ LockingTransaction t = transaction.get(); if(t == null || t.info == null) throw new IllegalStateException("No transaction running"); return t; } static public boolean isRunning(){ return getRunning() != null; } static LockingTransaction getRunning(){ LockingTransaction t = transaction.get(); if(t == null || t.info == null) return null; return t; } static public Object runInTransaction(Callable fn) throws Exception{ LockingTransaction t = transaction.get(); if(t == null) transaction.set(t = new LockingTransaction()); if(t.info != null) return fn.call(); return t.run(fn); } static class Notify{ final public Ref ref; final public Object oldval; final public Object newval; Notify(Ref ref, Object oldval, Object newval){ this.ref = ref; this.oldval = oldval; this.newval = newval; } } Object run(Callable fn) throws Exception{ boolean done = false; Object ret = null; ArrayList<Ref> locked = new ArrayList<Ref>(); ArrayList<Notify> notify = new ArrayList<Notify>(); for(int i = 0; !done && i < RETRY_LIMIT; i++) { try { getReadPoint(); if(i == 0) { startPoint = readPoint; startTime = System.nanoTime(); } info = new Info(RUNNING, startPoint); ret = fn.call(); //make sure no one has killed us before this point, and can't from now on if(info.status.compareAndSet(RUNNING, COMMITTING)) { for(Map.Entry<Ref, ArrayList<CFn>> e : commutes.entrySet()) { Ref ref = e.getKey(); if(sets.contains(ref)) continue; boolean wasEnsured = ensures.contains(ref); //can't upgrade readLock, so release it releaseIfEnsured(ref); tryWriteLock(ref); locked.add(ref); if(wasEnsured && ref.tvals != null && ref.tvals.point > readPoint) throw retryex; Info refinfo = ref.tinfo; if(refinfo != null && refinfo != info && refinfo.running()) { if(!barge(refinfo)) throw retryex; } Object val = ref.tvals == null ? null : ref.tvals.val; vals.put(ref, val); for(CFn f : e.getValue()) { vals.put(ref, f.fn.applyTo(RT.cons(vals.get(ref), f.args))); } } for(Ref ref : sets) { tryWriteLock(ref); locked.add(ref); } //validate and enqueue notifications for(Map.Entry<Ref, Object> e : vals.entrySet()) { Ref ref = e.getKey(); ref.validate(ref.getValidator(), e.getValue()); } //at this point, all values calced, all refs to be written locked //no more client code to be called long msecs = System.currentTimeMillis(); long commitPoint = getCommitPoint(); for(Map.Entry<Ref, Object> e : vals.entrySet()) { Ref ref = e.getKey(); Object oldval = ref.tvals == null ? null : ref.tvals.val; Object newval = e.getValue(); int hcount = ref.histCount(); if(ref.tvals == null) { ref.tvals = new Ref.TVal(newval, commitPoint, msecs); } else if((ref.faults.get() > 0 && hcount < ref.maxHistory) || hcount < ref.minHistory) { ref.tvals = new Ref.TVal(newval, commitPoint, msecs, ref.tvals); ref.faults.set(0); } else { ref.tvals = ref.tvals.next; ref.tvals.val = newval; ref.tvals.point = commitPoint; ref.tvals.msecs = msecs; } if(ref.getWatches().count() > 0) notify.add(new Notify(ref, oldval, newval)); } done = true; info.status.set(COMMITTED); } } catch(RetryEx retry) { //eat this so we retry rather than fall out } finally { for(int k = locked.size() - 1; k >= 0; --k) { locked.get(k).lock.writeLock().unlock(); } locked.clear(); for(Ref r : ensures) { r.lock.readLock().unlock(); } ensures.clear(); stop(done ? COMMITTED : RETRY); try { if(done) //re-dispatch out of transaction { for(Notify n : notify) { n.ref.notifyWatches(n.oldval, n.newval); } for(Agent.Action action : actions) { Agent.dispatchAction(action); } } } finally { notify.clear(); actions.clear(); } } } if(!done) throw new Exception("Transaction failed after reaching retry limit"); return ret; } public void enqueue(Agent.Action action){ actions.add(action); } Object doGet(Ref ref){ if(!info.running()) throw retryex; if(vals.containsKey(ref)) return vals.get(ref); try { ref.lock.readLock().lock(); if(ref.tvals == null) throw new IllegalStateException(ref.toString() + " is unbound."); Ref.TVal ver = ref.tvals; do { if(ver.point <= readPoint) return ver.val; } while((ver = ver.prior) != ref.tvals); } finally { ref.lock.readLock().unlock(); } //no version of val precedes the read point ref.faults.incrementAndGet(); throw retryex; } Object doSet(Ref ref, Object val){ if(!info.running()) throw retryex; if(commutes.containsKey(ref)) throw new IllegalStateException("Can't set after commute"); if(!sets.contains(ref)) { sets.add(ref); lock(ref); } vals.put(ref, val); return val; } void doEnsure(Ref ref){ if(!info.running()) throw retryex; if(ensures.contains(ref)) return; ref.lock.readLock().lock(); //someone completed a write after our snapshot if(ref.tvals != null && ref.tvals.point > readPoint) { ref.lock.readLock().unlock(); throw retryex; } Info refinfo = ref.tinfo; //writer exists if(refinfo != null && refinfo.running()) { ref.lock.readLock().unlock(); if(refinfo != info) //not us, ensure is doomed { blockAndBail(refinfo); } } else ensures.add(ref); } Object doCommute(Ref ref, IFn fn, ISeq args) throws Exception{ if(!info.running()) throw retryex; if(!vals.containsKey(ref)) { Object val = null; try { ref.lock.readLock().lock(); val = ref.tvals == null ? null : ref.tvals.val; } finally { ref.lock.readLock().unlock(); } vals.put(ref, val); } ArrayList<CFn> fns = commutes.get(ref); if(fns == null) commutes.put(ref, fns = new ArrayList<CFn>()); fns.add(new CFn(fn, args)); Object ret = fn.applyTo(RT.cons(vals.get(ref), args)); vals.put(ref, ret); return ret; } /* //for test static CyclicBarrier barrier; static ArrayList<Ref> items; public static void main(String[] args){ try { if(args.length != 4) System.err.println("Usage: LockingTransaction nthreads nitems niters ninstances"); int nthreads = Integer.parseInt(args[0]); int nitems = Integer.parseInt(args[1]); int niters = Integer.parseInt(args[2]); int ninstances = Integer.parseInt(args[3]); if(items == null) { ArrayList<Ref> temp = new ArrayList(nitems); for(int i = 0; i < nitems; i++) temp.add(new Ref(0)); items = temp; } class Incr extends AFn{ public Object invoke(Object arg1) throws Exception{ Integer i = (Integer) arg1; return i + 1; } public Obj withMeta(IPersistentMap meta){ throw new UnsupportedOperationException(); } } class Commuter extends AFn implements Callable{ int niters; List<Ref> items; Incr incr; public Commuter(int niters, List<Ref> items){ this.niters = niters; this.items = items; this.incr = new Incr(); } public Object call() throws Exception{ long nanos = 0; for(int i = 0; i < niters; i++) { long start = System.nanoTime(); LockingTransaction.runInTransaction(this); nanos += System.nanoTime() - start; } return nanos; } public Object invoke() throws Exception{ for(Ref tref : items) { LockingTransaction.getEx().doCommute(tref, incr); } return null; } public Obj withMeta(IPersistentMap meta){ throw new UnsupportedOperationException(); } } class Incrementer extends AFn implements Callable{ int niters; List<Ref> items; public Incrementer(int niters, List<Ref> items){ this.niters = niters; this.items = items; } public Object call() throws Exception{ long nanos = 0; for(int i = 0; i < niters; i++) { long start = System.nanoTime(); LockingTransaction.runInTransaction(this); nanos += System.nanoTime() - start; } return nanos; } public Object invoke() throws Exception{ for(Ref tref : items) { //Transaction.get().doTouch(tref); // LockingTransaction t = LockingTransaction.getEx(); // int val = (Integer) t.doGet(tref); // t.doSet(tref, val + 1); int val = (Integer) tref.get(); tref.set(val + 1); } return null; } public Obj withMeta(IPersistentMap meta){ throw new UnsupportedOperationException(); } } ArrayList<Callable<Long>> tasks = new ArrayList(nthreads); for(int i = 0; i < nthreads; i++) { ArrayList<Ref> si; synchronized(items) { si = (ArrayList<Ref>) items.clone(); } Collections.shuffle(si); tasks.add(new Incrementer(niters, si)); //tasks.add(new Commuter(niters, si)); } ExecutorService e = Executors.newFixedThreadPool(nthreads); if(barrier == null) barrier = new CyclicBarrier(ninstances); System.out.println("waiting for other instances..."); barrier.await(); System.out.println("starting"); long start = System.nanoTime(); List<Future<Long>> results = e.invokeAll(tasks); long estimatedTime = System.nanoTime() - start; System.out.printf("nthreads: %d, nitems: %d, niters: %d, time: %d%n", nthreads, nitems, niters, estimatedTime / 1000000); e.shutdown(); for(Future<Long> result : results) { System.out.printf("%d, ", result.get() / 1000000); } System.out.println(); System.out.println("waiting for other instances..."); barrier.await(); synchronized(items) { for(Ref item : items) { System.out.printf("%d, ", (Integer) item.currentVal()); } } System.out.println("\ndone"); System.out.flush(); } catch(Exception ex) { ex.printStackTrace(); } } */ }