Seyed Masoud Hosseini · Overview · Study log · Ideas · Transcript · RSS feed

Parallel Computing & CUDA · Lecture 16 of 19 · 1:20:20

Lecture 16: Transactional Memory 1

Stanford CS149 I Parallel Computing I 2023 I Lecture 16 - Transactional Memory 1 on YouTube

Study guide

What this lecture covers

This lecture opens a two-part unit on transactional memory, motivated by the difficulty of writing correct, high-performance synchronization with locks: coarse-grain locks are easy but limit concurrency, while fine-grain locks can give good performance but are hard to get right and don't compose. The lecture introduces transactions as a declarative alternative, works through their formal properties, and begins covering how an implementation actually provides those guarantees.

It builds on earlier lectures covering atomic operations, cache coherence, and lock-based synchronization, and sets up the second transactional memory lecture, which covers software and hardware implementations in more depth. After watching, you should be able to explain what atomicity, isolation, and serializability mean for a transaction, why locks break composability while transactions don't, and how a system distinguishes conflicting from non-conflicting transactions using read and write sets.

Key ideas

  • Coarse- vs. fine-grain locking tradeoff: locking a whole data structure is simple but serializes all access; locking small pieces (e.g., per bucket, hand-over-hand on a tree) allows more concurrency but is complex and can be dominated by locking overhead at low thread counts.
  • Atomic as a declarative construct: writing atomic { ... } states what behavior is required (the block executes as one indivisible, isolated unit) without specifying how the system achieves it, unlike explicit lock acquire/release, which is imperative.
  • Transaction semantics: modeled on database ACID properties minus durability — atomicity (all or nothing), isolation (no other transaction observes intermediate state), and serializability (the system can order all transactions into some valid serial order, giving effectively sequential consistency).
  • Read set and write set: the addresses a transaction reads and writes; two transactions conflict only if their read/write or write/write sets intersect, which is what lets independent operations (like updating different tree nodes) run concurrently even though both are wrapped in atomic.
  • Composability: locks require a global lock-ordering convention to avoid deadlock when combining operations (e.g., nested account transfers), which breaks modularity; nested transactions simply subsume into the outer atomic block without that coordination.
  • Atomicity violations: wrapping the wrong code in atomic, or relying on cross-transaction communication that isolation forbids, produces incorrect programs (such as livelock) even though the syntax looks correct.
  • Data versioning: how uncommitted writes are tracked — eager versioning updates memory immediately and keeps an undo log for aborts (fast commit, slow abort), while lazy versioning buffers writes and applies them only at commit (slow commit, fast abort).
  • Conflict detection: pessimistic (encounter-based) detection checks for conflicts on every memory access and can stall or abort immediately; optimistic detection defers all checking to commit time, comparing the committing transaction's write set against other transactions' read sets.

Walkthrough

Why locking alone is hard (0:05)

The instructor reviews that atomic load and store are the hardware primitives underlying locks, barriers, and lock-free data structures, but notes that programming directly with these primitives forces a tradeoff: coarse-grain locks are correct but slow because they serialize access, while fine-grain locks can be fast but are error-prone and hard to implement correctly on complex data structures. This sets up transactions as a way to get both correctness and performance without hand-managing lock granularity.

Atomic as a declarative alternative to locks (6:13)

Using a bank deposit example, the lecture contrasts explicitly acquiring and releasing a lock with simply wrapping the same code in an atomic block. Atomic is declarative — it states the required behavior (this sequence executes as one unit) and leaves the implementation to the system, paralleling the earlier distinction between declarative constructs like an ISPC foreach and imperative constructs like explicitly spawned worker threads.

ACID-like semantics for transactions (10:19)

Borrowing from database transactions, the lecture defines the properties a transaction must satisfy: atomicity (all reads and writes take effect, or none do), isolation (no other transaction can observe a transaction's reads or writes before it commits), and serializability (the system can order all transactions into some valid serial sequence, though the programmer does not control that order). Durability is absent since memory isn't persistent. Together these amount to sequential consistency applied at the granularity of whole transactions rather than individual memory operations.

Motivating performance with a hashmap and a tree (14:24)

A Java hashmap example shows that a single lock around the whole structure barely improves with more threads, while per-bucket fine-grain locks scale but carry enough overhead to actually perform worse than coarse-grain locking at low thread counts. A second example walks through updating two different nodes of a tree using hand-over-hand locking versus transactions: by comparing each transaction's read set and write set for the two updates, the lecture shows there is no intersection, so the updates could safely run concurrently, and a transactional memory system with hardware support can capture that concurrency automatically, matching or beating fine-grain locking.

Composability and atomicity violations (29:46)

A nested account-transfer example (transfer from A to B while another thread transfers from B to A, each implemented with per-account locks acquired in different orders) produces deadlock, and the only general fix is a global lock-ordering convention that breaks the composability of smaller modules into larger ones. Transactions avoid this: nested atomic blocks are simply absorbed into the outer transaction, and if two transfers touch disjoint accounts they can run concurrently, while transfers that overlap are serialized automatically. The lecture also shows that atomic is not a magic fix — using it to replace synchronization that depends on threads observing each other's writes before commit (violating isolation) produces livelock, and wrapping the wrong scope of code in atomic can still leave atomicity violations, such as dereferencing a pointer set to null by an unprotected statement outside the atomic block.

Implementing data versioning (44:08)

Moving from abstraction to implementation, the lecture identifies data versioning (how uncommitted and committed state are managed) as one axis of a transactional memory implementation. Eager versioning writes directly to memory as soon as possible and records the old value in an undo log, so commits are fast (just discard the log) but aborts are slower (replay the undo log to restore memory). Lazy versioning instead buffers writes in a write buffer, leaving memory untouched until commit, so aborts are fast (discard the buffer) but commits are slower (flush the buffer to memory), and reads inside the transaction may need to check the write buffer as well as memory.

Pessimistic vs. optimistic conflict detection (56:17)

The second implementation axis is conflict detection. Pessimistic (encounter-order) detection checks for a conflict on every memory access; under an aggressive contention manager where writers always win, a conflicting read causes the reader to stall (preserving work done so far) while a transaction reading data that is later overwritten must abort and restart, and the lecture works through several access sequences to show when transactions stall, abort, or reach a livelock that the system must detect and break. Optimistic detection instead defers checking until a transaction wants to commit, comparing its write set against the read sets of other active transactions; a conflict there forces the other transaction to abort and restart (a "doomed transaction"), while non-conflicting commits proceed immediately, which can achieve forward progress in cases where pessimistic detection makes none.

Before you watch

  • Review the earlier lectures on atomic compare-and-swap, locks, and cache coherence, since transactional memory builds directly on those synchronization primitives.
  • Be comfortable with the idea of hand-over-hand (fine-grain) locking on a tree or linked list, which this lecture uses as a running comparison point.

Check your understanding

  1. What does it mean for atomic to be a declarative construct, and how does that differ from explicitly acquiring and releasing a lock?
  2. Why do two transactions with disjoint read and write sets not conflict, and how does that let a transactional memory system beat coarse-grain locking?
  3. Why does nesting locks require a global lock-ordering convention to avoid deadlock, while nesting atomic blocks does not have that problem?
  4. Compare eager and lazy data versioning: which is faster to commit, which is faster to abort, and why?
  5. Under pessimistic conflict detection with a writer-wins contention manager, what is the difference between a transaction stalling and a transaction aborting?

From the YouTube description

Motivation for transactions, design space of transactional memory implementations.

To follow along with the course, visit the course website:
https://gfxcourses.stanford.edu/cs149/fall23/

Kayvon Fatahalian
Associate Professor of Computer Science, Stanford University
https://graphics.stanford.edu/~kayvonf/

Kunle Olukotun
Cadence Design Systems Professor, Professor of Electrical Engineering and of Computer Science, Stanford University
https://engineering.stanford.edu/people/oyekunle-olukotun

Learn more about the online course and how to enroll: https://online.stanford.edu/courses/cs149-parallel-computing

To view all online courses and programs offered by Stanford, visit: https://online.stanford.edu/

← Lecture 15: Domain-Specific Programming Languages · Lecture 17: Transactional Memory 2 →