Notes for CSC 2/458, 13--15 April 2026 Transactional Memory Problems with locks forget to lock; forget to release deadlock, priority inversion lack of composability * complexity/concurrency tradeoff also preemption, page faults, thread failure, but that's an orthogonal set of issues TM addresses the first bunch of these with higher-level abstraction. A few implementations address the latter as well (by being nonblocking). Basic idea borrowed from DB community. Merger of two key ideas programming model based on high-level atomic blocks implementation based on speculation HW v. SW all things equal, HW is faster, but HW policies are hard to change SW policies can be more complex, more dynamic legacy machines need SW HW impls need SW fall-back hybrids best effort acceleration Background Herlihy and Moss, ISCA'93 (Hardware TM) Stone et al. IEEE PDT'93 (Oklahoma Update) Shavit & Touitou, PODC'95 (Software TM) (one of several important theory papers from that era) static transactions, lock-free, orec-based Harris & Fraser, OOPSLA'03 (WSTM) dynamic transactions, obstruction free, conditional critical regions notation, orec-based Herlihy, Luchangco, Moir, and Scherer, PODC'03 (DSTM) object-based; obstruction-free Cf. OSTM (Fraser '03 thesis): object-based; lock-free (via helping) ASTM (Marathe et al. DISC'05): adaptive elimination of indirection RSTM (Marathe et al. TRANSACT'06): eliminates locators entirely Rajwar & Goodman, MICRO'01, ASPLOS'02 (SLE) transpartent speculation - co-execute nonconflicting critical sections (coarse-grain locks) - avoid missing on the lock itself (stays shared) Martinez & Torrellas, ASPLOS'02 (Speculative Synchronization) explicit locks (or barriers); one safe thread, rest speculate Most of the focus in the notes below is on shared locations: how do we update shared variables atomically? To support the atomic{ } block syntax, however, transactions that abort should roll back and retry automatically and transparently, which means local variables need to revert as well. That almost certainly requires compiler support -- as does integration w/ exceptions, libraries, non-transactional synchronization, etc. --------------------- Dimensions to the design space (incomplete list) blocking/nonblocking (lock-free v. obstruction-free) OSTM was lock free; several others obstruction-free; most blocking object-based/stripe (orec)-based buffering redo/undo/cloning access tracking & conflict resolution eager/lazy (latter complicates seeing one's own writes) mixed: WW eager, RW lazy both reader & writer can commit if reader goes first visible readers/invisible readers/partially-visible readers (SNZI) Aside: SNZI maintains a tree in which a thread sets a flag in its leaf and, recursively, in the parent if not set already [2nd aside: mindicator] validation (to maintain consistency & avoid bizarre behavior) underappreciated by some early systems; implement with - incremental open-time checks - post-read time-based checks (ala TL2) commit counter heuristic (Spear et al.): skip validation if no one has committed writes - sandboxing contention management (matters mainly in the eager case) lots of options, based on random backoff, who started first, who is farther along, who has aborted the larger # of times also who wins, and whether the loser aborts or retries have to deal with starvation -- grab global lock eventually? Representative styles of SW implementation (1) eager, undo, R-W locking, object-based (Ennals, McRT, Bartok) keep time-sorted write log, undo in reverse order (2) eager, NB, cloning, incrementally validating, object-based, obstruction-free, vis or invis readers (OSTM/DSTM/ASTM/RSTM) DSTM is easiest to describe OSTM is kinda similar, but with full intent list visible to peers, so they can help and get lock freedom ASTM avoids one level of indirection in the common case; DSTM avoids another (3) lazy, redo, time-based, stripe (or object) based, read-post-validating TL2 [Dice et al, DISC'06] abort if location has been modified since transaction started TinySTM [Felber et al., PPoPP'08] perform full validation and update of start time instead More on validation Required with invisible readers Naive approach validates all past reads on each new one i.e., by checking sequence numbers on orecs O(n^2) total time for an n-read transaction TL2 innovation: use timestamps on orecs. If to-be-read location has not been modified since my txn began, it's guaranteed to be consistent with all previous reads, and I'm good -- O(n) time LSA (TinySTM) innovation: if to-be-read location (has) been modified since my txn began, check to see if nothing _else_ I've read has been modified since then. If so, update notion of "start time" and proceed. Same time complexity as TL2 when all goes well; fewer restarts when it doesn't all go well. NB: The lock on an orec is either the time of the most recent update or the id of the thread trying to make updates (one bit is used to distinguish between the two cases). At completion, a txn has to unlock all its orecs. (4) lazy, redo, non-locking, commit-serializing RingSTM [Spear et al., SPAA'08] low and high watermarks in (conceptually unbounded) list remember high as of start time; double-check on read; validate if nec. keep read and write filters Aside: Bloom filters commit by enqueuing write filter NOrec [Dalessandro et al., PPoPP'10] single global sequence lock for commits value-based validation no orecs in which to keep timestamps or other metadata! but it's ok -- no ABA: VBV means the transaction can "happen instantaneously" now read-only transactions acquire no locks at all hashed write set for RAW lookups naturally privatization safe (5) TML: speculation with a global sequence lock very lightweight instrumentation on begin, end, write, and (dangerous) read. full concurrency among readers one writer at a time basically a formalization of seqlocks to put lock-based validation after every dangerous read --------------------- Hardware lots of proposals six (publicly known) real implementations: Azul, Rock, BG/Q, Haswell, zEC12, p8 ARM v9 has HTM; no silicon yet, but GEM5 simulation Representative HW implementations eager, responder loses (livelock?), with speculative stores kept in Write buffer (Rock) L1 cache (Azul, Haswell) "gathering" cache below L2 (z) Speculation buffer (H&M, ASF, p) L2 cache (L1 pass-through), multiversioning, SW chooses winner (BG/Q) Fully lazy, with central arbiter (TCC) distributed commit protocol (TCC2) continuous conflict monitoring (FlexTM) Interesting wrinkles HLE on Intel constrained txns on z non-txnal loads and stores both, eagerly, on ASF stores, lazily, on z loads, eagerly, on Sapphire Rapids suspend/resume on p Potential uses of non-txnal loads and stores: save statistics on aborted txns ordered txns open nesting, for GC, memoization, dynamic linking, malloc/free footprint minimization read-only "planning" (cf.: early release) staggered txns (advisory lock) lazy subscription cooperation w/ concurrent peers boosting ---------------------------------------- HLE examples TATAS lock is trivial: put XACQUIRE before TAS; XRELEASE before release store. Ticket lock is only slightly more complicated. Original version: class lock atomic next_ticket := 0 atomic now_serving := 0 const int base = ... lock.acquire(): int my_ticket := next_ticket.FAI() loop int ns := now_serving.load() if ns = my_ticket break pause(base * (my_ticket - ns)) lock.release(): int t := now_serving.load() + 1 now_serving.store(t) SLE version of acquire is unchanged except for XACQUIRE on FAI. Release becomes lock.release(): int ns := now_serving.load() if ! XRELEASE next_ticket.CAS(ns+1, ns) now_serving.store(ns+1) Will work fine on machines where the X prefix bytes are no-ops. On an HLE-equipped machine, inspection of the lock will see the speculatively updated version. ============================ Semantic challenges ---------------------------------------- Privatization: deals with non-txnal access after txnal access. Example: atomic { atomic { if (visible) visible = false if (p != null) } if (p != null) p = p->next p = p->next < abort > Here thread 2 may suffer a memory violation if the code interleaves as shown and it doesn't realize fast enough that the commit in thread 1 is going to force it to abort. This is the DOOMED TRANSACTION manifestation of the privatization problem. Similarly: atomic { if (visible) if (p != null) p = p->next < commit > atomic { visible = false } if (p != null) < clean up > p = p->next } If thread 2 is using a redo log, thread 1 may not see the update of p in time, and may suffer a memory violation. This is the DELAYED CLEANUP manifestation of the privatization problem. There are symmetric versions of both doomed transaction and delayed cleanup for systems with an undo log. There is also a symmetric publication problem at the _beginning_ of transactions, but only in programs with data races. ---------------------------------------- Fundamental semantics The most widely discussed formalization is _opacity_ (Guerraoui & Kapalka), which requires transactions to abort the instant the become inconsistent. I strongly dislike that model, because real systems have zombies. Proposal for C++ is basically an explanation based on locks. Here at Rochester, we have proposed an (almost but not quite equivalent) model. At the program level: implementation is correct if we have TSC (apparent global serialization order for transactions) for TDRF programs (all conflicting accesses inside transactions). Locking and other traditional sync defined _in terms of_ atomic blocks: class lock void acquire( ) Boolean held := false while true atomic void release( ) if not held atomic held := true held := false return There is no program-level notion of speculation or of "aborted transactions". At the runtime level, an implementation is correct if we have a - memory model - conflict function - (optional) arbitration function such that - we can prove that every execution has a sequential equivalent (the "fundamental theorem of TM") - we can prove minimal liveness -- specifically, try-commit fails only in the presence of some conflicting txn - a read r in an unsuccessful txn T is inconsistent with previous reads of T only if there exists some other txn S whose prefix prior to r conflicts with T (giving T an excuse to abort) - zombie executions are bounded: there exists a k such that if T can make k more steps, it can do so consistently - exceptions (if any) never escape an unsuccessful txn Many problems with lock-based TM semantics go away if we have TM-based lock semantics instead. Example (from Shpeisman et al.): initially v == w == 0 T1: atomic { T2: v := 1 while (v != 1) { } w := 1 while (w != 1) { } } (To avoid formal data race, label w and v as atomic and make the accesses memory_order::relaxed.) With single-lock TM semantics, this program can terminate. With TM-based lock semantics, it does not (and should not). For better performance, can pay for privatization and publication only when needed by labeling transactions as acquiring, releasing, neither, or both. ---------------------------------------- irreversible operations for correctness (e.g. read-after-prompt interactive I/O) for speed (avoid instrumentation in inevitable txn) implement via global read/write lock no concurrency global write lock allows concurrent readers, but inev. txn must acquire have to instrument inev. reads to wait for concurrent writers to abort GWL + "transactional (quiescence) fence" avoids inev. read instrumentation transactional "drain" wait only for concurrent writers to finish, not all txns basically the prev. option plus concurrent readers inevitable read locks allows concurrent writers, but forces read instrumentation in the inevitable txn inevitable read Bloom filter cheaper way to allow concurrent writers commit order == serialization order == cleanup order (RingSTM) ---------------------------------------- condition sync retry possible speculation-free (but non-operational) semantics: txn serializes at a point in the execution where retry will never happen (retry as "while true") perhaps recast as "require " ---------------------------------------- nesting subsumption closed semantically necessary if you have cancel() how to tell what level you conflict with? open admits circularity x == y == 0 A B atomic { atomic { open atomic { open atomic { x = 1 y = 1 } } u = y v = x } } u == v == 1 // neither outer txn came first Cf: non-txnal loads and stores ---------------------------------------- generalizing conflict malloc & free boosting ---------------------------------------- exceptions error v. non-local return ---------------------------------------- debugging how do you see what's inside something indivisible? ---------------------------------------- additional compiler challenges viral character of "txn-safe" elision of validation memory fences on relaxed order machines instrumentation on provably non-escaping values tracking irreversible operations require programmer to label? what's the default: reversible or irreversible? if the former, what do you do about legacy libraries? if the latter, you have to label a LOT efficient sandboxing maybe track "transactional state" of references clone subroutines based on states of parameters maybe repair / partial rollback ======================================== Hybrid systems Big challenge: HW txn needs to "subscribe" to SW lock to avoid accidental commit. Matveev & Shavit [SPAA'13]: reduced HW txns 3 levels: - all HW (fast) w/ no instrumentation other than read of the global clock (written by slow-slow path) - commit-in-HW (slow) - all SW -- TL2-like (slow-slow) fast and slow can coexist, as can slow-slow and modified (slower) fast mode that implements full TL2 Shriraman et al.: in-cache buffering of speculative lines conflict summary tables (Bloom filters) for eager conflict _detection_ alert-on-update for immediate notification -- e.g., of peers