Software Perspectives on Transactional Memory Ben Liblit CS 838-3
Architects Keep PL People Busy You gave us instruction sets We made high level languages You gave us RISC instead We made optimizing compilers You gave us context switching We made a big ugly mess: threads + locks You may soon give us transactions Ooh! New toy! What can we make with it?
Are Locks Really That Bad? Yes, locks are really that bad Entire careers built on making them less awful But not a problem for most programmers (!) Most aren’t crazy enough to use threads Used only in restricted contexts, or by specialized “mad genius” programmers Need something better Maybe transactions are that something
Transactions and Language Design Look at two key design questions Control structures Non-transactional operations (“magic”) Assorted issues & tricky bits Running themes Compositionality What happens in hardware/software?
Control Structure: Atomic Blocks ... } Obvious stuff Start trans on entry Commit trans on exit Nesting? We will nest, whether hardware likes it or not Compositionality! Locks are deeply broken here If we need to use counters to get nesting, we will. Java synchronized method optimization one version that actually acquires lock one version for use when lock is statically known to be already held e.g. called from other synchronized method
Conditional Critical Regions atomic (g) { work(); } Wait until g is true Useful where condition variables might be used What can guard be? Just a memory word? Negated? Arbitrary expression? …with side effects? Variant: wait until g changes.
CCRs Using Low-Level Ops atomic (g) { work(); } done = false; while (!done) { begin(); if (g) { work(); done = commit(); } else wait(); }
Implementing wait() Abort transaction Back to begin() Spin wait, or… Hardware assist! Track locations read Wait for write to any of them Can you do this? done = false; while (!done) { begin(); if (g) { work(); done = commit(); } else wait(); } Note: also needs a commit() that tells is whether it succeeded or failed. How much of this is done by hardware?
Managing Exceptions Abort or commit? atomic { ... throw foo; } Abort or commit? If abort, what happens to exception object? Consensus: commit Just one more way to exit a block of code Nothing special from programmer’s standpoint, but… “foo” might be… a newly allocated object a preexisting object modified in the atomic block linked in with other data structures connected to some library invariant How do you handle rollback of this stuff while still having an exception to throw?
Managing Exceptions atomic { ... throw foo; } done = false; while (!done) { begin(); try { work(); done = commit(); } catch (t) { if (done) throw t; } Requires basically what we’ve already seen. But if hardware was helping before, can it still help here? Can hardware still help?
Can I please have abort() too? void move(Map src, Map dest, int key) { atomic { try { Object val = src.remove(key); dest.insert(key, val); } catch (MapFullException e) { src.insert(key, val); } Don’t say “yes” too quickly: this has major implications for sequential implementations. Am I getting too greedy? Does this exceed the transactional mandate?
Speculation and Validation atomic { if (x != y) while (1) {} } atomic { ++x; ++y; } If right commits between left reads … stuck! Either don’t speculate or add periodic validation Is your favorite transactional hardware vulnerable? Can hardware help here, or is this compiler’s job?
Speculation and Validation atomic { if (x != y) launchMissiles(); } atomic { ++x; ++y; } If right commits between left reads … stuck! Either don’t speculate or add periodic validation Is your favorite transactional hardware vulnerable? Can hardware help here, or is this compiler’s job?
Control Structures: API Recap void begin(); // basics bool commit(); // basics void wait(); // CCRs void abort(); // greedy? bool validate(); // speculate
Non-Transactional Operations All software has “magic” on the edges Magical operations outside system definition Walk off the edge? No coming back! Operations which cannot be rolled back Operating system: physical world User code: system calls Bytecode: native methods / system calls (?)
Not All Magic Is Alike! Standard examples Input/output Memory allocation Query current time Thread creation Can/should these be rolled back? Can user code help hardware “fake it”?
Some Unattractive Options Forbid magic inside atomic blocks Non-compositional Limits transactional memory to tiny tasks Detect violations at compile time / run time? Ban speculation for blocks with magic Compositionality survives Sacrifice performance What about our beloved abort() call?
Compensation Code Magic + compensation == transactional (Well, we pretend it is) Software, not hardware Example: buffering Delicate interaction with hardware roll-back Language/library designers must Decide on abstract machine model Build compensations consistent with model Compensation code has effects which escape from roll-back Compensations could be used to implement transaction retry counters! Let you give up if you’re retried too many times and not gotten anywhere. For example, does abstract machine model let you treat addresses as numeric values?
Ben’s Free Prognostication Will see a mix of strategies Virtual compensation in virtual machines Initially, dynamic enforcement Or no enforcement at all! Eventual development of static program analyses to prevent dynamic violations
Atomic With Respect To… Other threads in same process? Other atomic blocks in same process? But not threads which are outside atomic blocks Other processes on same machine? Consider shared memory-mapped pages Consider shared file system Connections to security, journaling file systems
Granularity of Conflict Detection Difference between address and “thing” Which of these can cause roll-backs? int x, y; struct { int x, y; } point; long long int xy; Shame to expose machine-level details But precedents exist in C and even Java Suppose one thread changes the 0th bit of xy, and another changes the 63rd bit. Is there a conflict?
Garbage Collectors Read large amounts of memory Likely to clash with everything else Cause transaction thrashing? Concurrent garbage collection We know how to do this now, but it wasn’t easy! How will we do it with transactions? Future employment opportunity for “mad genius” programmers who used to do fine-grained lock coding! Do you run finalizers when you roll back object creation?
Conclusions Locks are a nightmare; time to wake up Atomicity may be the right abstraction Study: 80% of Java methods are atomic Push years of PL research into the archives But we’ll thank you for it Atomicity is nicer for program analysis too! Compositionality is critical Static analysis of concurrent systems is extremely difficult. Atomicity enables sequential reasoning about program behavior.