Introduction

Every computer you own runs dozens of threads at once. Each thread reads and writes shared memory — a bank balance, a cache entry, a game state — and if two threads collide on the same location at the same time, the result can be a corrupted value that no programmer intended.

The classic solution is the lock: before touching shared data, grab a mutex; when done, release it. Locks work, but they are pessimistic. They assume a conflict is about to happen and make every thread wait, even when conflicts are rare.

Transactional memory (TM) takes the opposite bet. You mark a block of code as a transaction — an atomic region — and simply run it, reading and writing speculatively. The hardware or runtime watches every memory access. If it detects that another thread has touched the same location, the transaction aborts and retries from scratch. If no conflict happened, the transaction commits and all its writes become visible at once.

The analogy is a database transaction: you get atomicity (all or nothing), isolation (invisible mid-flight), and consistency (no partial updates). Unlike a database, transactional memory works at the granularity of individual machine words, in nanoseconds, without a query language in sight.

Try It: Conflict & Retry

The demo below simulates two concurrent transactions that each want to increment the same shared counter. Press Run transactions to start. Each transaction reads the counter speculatively, increments it in its own private buffer, then tries to commit.

<!-- {{c_html_intro}} -->
<div class="tm-panel">
  <div class="counter-row">
    <span class="label">{{lbl_counter}}</span>
    <span id="counter" class="counter-val">0</span>
  </div>
  <div class="txn-area">
    <div class="txn-box" id="box-a">
      <div class="txn-title">{{lbl_txn_a}}</div>
      <div class="txn-log" id="log-a"></div>
    </div>
    <div class="txn-box" id="box-b">
      <div class="txn-title">{{lbl_txn_b}}</div>
      <div class="txn-log" id="log-b"></div>
    </div>
  </div>
  <div class="status-row" id="status"></div>
  <div class="btns">
    <button id="btn-run" type="button">{{btn_run}}</button>
    <button id="btn-reset" type="button" class="ghost">{{btn_reset}}</button>
  </div>
  <div class="hint-row">{{hint_text}}</div>
</div>
/* {{c_css_intro}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.tm-panel { padding: .5rem; }
.counter-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .8rem; }
.label { font-size: .9rem; color: #555; }
.counter-val { font: 700 2rem ui-monospace, monospace; color: #1d3557;
               background: #e8eef3; border: 1px solid #cdd9e3;
               border-radius: 8px; padding: .1rem .6rem; min-width: 3rem; text-align: center; }
.txn-area { display: flex; gap: .7rem; margin-bottom: .7rem; }
.txn-box { flex: 1; border: 1px solid #cdd9e3; border-radius: 10px;
           padding: .5rem .7rem; min-height: 120px; }
.txn-box.committing { border-color: #0a7d33; background: #f0faf3; }
.txn-box.aborting   { border-color: #c92f3c; background: #fff3f4; }
.txn-box.committed  { border-color: #0a7d33; background: #e6f7ea; }
.txn-title { font: 700 .85rem system-ui; color: #1d3557; margin-bottom: .35rem; }
.txn-log { font: .78rem ui-monospace, monospace; line-height: 1.6; }
.log-read    { color: #1d3557; }
.log-write   { color: #0a7d33; }
.log-abort   { color: #c92f3c; }
.log-commit  { color: #0a7d33; font-weight: 700; }
.log-retry   { color: #e76f00; }
.status-row { font: 600 .95rem system-ui; min-height: 1.4em; margin-bottom: .5rem; }
.status-ok  { color: #0a7d33; }
.status-bad { color: #c92f3c; }
.status-run { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.hint-row { font-size: .78rem; color: #666; line-height: 1.5; }
// Code not found

When both transactions read the same initial value and try to commit simultaneously, one detects the conflict and aborts, rolling back its speculative writes and retrying from the current value. The other commits first and its result becomes permanent. Watch the retry count and notice that after at most a handful of rounds both transactions always commit — no deadlock is possible.

The Real Complexity

Transactional memory sounds like a free lunch. It is not.

  • Conflict detection has a cost. Hardware TM (HTM) tracks a read-set and a write-set in cache lines. When another core invalidates a tracked cache line, the transaction aborts. This is fast, but an abort means redoing all the work — potentially many times under high contention.
  • Capacity limits exist. HTM transactions must fit in L1 or L2 cache. An access that evicts a tracked line triggers an abort unrelated to any actual conflict. Software TM (STM) removes this limit but adds runtime overhead per access.
  • Abort rates blow up under contention. If kk threads all contend on the same location, the expected number of retries before one commits grows as O(k)O(k), and throughput can collapse — exactly the pathological case where a coarse lock would have been faster.
  • Irrevocable operations cannot be speculative. I/O, system calls, and anything visible outside the CPU cannot be rolled back. These must fall back to a lock or a special "irrevocable" mode, punching a hole in the abstraction.
  • Progress guarantees are subtle. HTM offers no liveness guarantee: a transaction can abort indefinitely on a hot cache line. STM implementations must provide a fallback path — typically a global lock — so that every transaction eventually commits.

The theoretical picture: deciding whether a set of transactions can commit without conflict is related to scheduling theory and serializability, topics at the heart of database concurrency control. Whether hardware or software, transactional memory trades one set of hard problems (deadlock, priority inversion, lock ordering) for another (abort storms, capacity aborts, irrevocability).

Where It Matters

Transactional memory has moved from theory into production in several forms:

  • Hardware TM in processors: Intel's TSX (Transactional Synchronization Extensions), introduced in Haswell (2013), brought HTM to x86. IBM's POWER and zSeries architectures ship their own variants. These let ordinary C/C++ code wrap critical sections in xbegin/xend with no library required.
  • Software TM in functional languages: Haskell's STM (introduced by Harris et al., 2005) is the cleanest production STM ever built. Transactions are first-class values in the STM monad; the type system prevents I/O inside a transaction at compile time, eliminating the irrevocability problem by construction.
  • Concurrent data structures: lock-free and wait-free structures — queues, hash maps, trees — can be built on TM primitives without hand-crafting compare-and-swap sequences. The result is correct-by-construction structures with performance close to hand-tuned equivalents.
  • Databases and storage engines: MVCC (multi-version concurrency control) in PostgreSQL, MySQL InnoDB, and others is a form of optimistic concurrency at the row level. The ideas are the same: speculate, detect conflict at commit, abort or retry.
  • Parallel algorithms: graph algorithms, simulation loops, and compilers use TM to parallelize phases that are mostly independent, accepting the occasional abort as cheaper than partitioning the work perfectly.

Understand transactional memory and you have unlocked a key design axis in every concurrent system: optimistic vs. pessimistic, and the tradeoffs that determine which wins.

Conclusion

Transactional memory turns a hard problem — coordinating threads without corrupting shared state — into an optimistic bet: assume the conflict won't happen, run speculatively, and pay only when the bet is wrong. In low-to-moderate contention, that bet almost always pays off, and the result is simpler, faster, and deadlock-free concurrent code.

The deeper lesson is that pessimism and optimism are not moral choices but engineering trade-offs. Locks shine when conflicts are frequent and expensive to redo. Transactions shine when conflicts are rare. Real systems — from your CPU's HTM to Haskell's STM monad to PostgreSQL's MVCC — have quietly embraced this duality, and knowing when to trust the optimistic path is one of the sharpest tools in a systems programmer's kit.

For the theoretical context that makes this precise, see scheduling and the broader P vs NP landscape that frames how hard concurrent consistency really is.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/transactional-memory/Content licensed under CC BY-NC 4.0.