Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Why STM? — The Shared State Problem

Consider transferring money between two accounts. With mutexes:

fn transfer(from: &Mutex<Account>, to: &Mutex<Account>, amount: u64) {
    let from_guard = from.lock().unwrap();
    // Thread B might be doing transfer(to, from, ...) right here
    let to_guard = to.lock().unwrap();
    // DEADLOCK: Thread A holds from, waiting for to.
    //           Thread B holds to, waiting for from.
    from_guard.balance -= amount;
    to_guard.balance += amount;
}

The standard fix (always lock in a consistent order) requires global coordination across your codebase. Add a third account and you need to sort three locks. It doesn’t compose.

STM: Optimistic Concurrency

STM operates on the assumption that conflicts are rare. Instead of locking, it:

  1. Reads current values into a local transaction log
  2. Computes new values based on those reads
  3. Attempts to commit: checks that nothing changed since the reads, then atomically writes

If anything changed between step 1 and step 3, the transaction retries automatically from step 1.

use effectful::{Effect, Stm, TRef, commit};

fn transfer(from: TRef<Account>, to: TRef<Account>, amount: u64) -> Effect<(), TransferError, ()> {
    let transaction: Stm<(), TransferError> = from.read_stm().flat_map(move |from_acct| {
        to.read_stm().flat_map(move |to_acct| {
        if from_acct.balance < amount {
            Stm::fail(TransferError::InsufficientFunds)
        } else {
            from.write_stm(Account { balance: from_acct.balance - amount, ..from_acct })
                .flat_map(move |_| to.write_stm(Account { balance: to_acct.balance + amount, ..to_acct }))
        }
        })
    });

    commit(transaction)
}

No locks. No deadlock risk. The transaction retries automatically if another transaction modified either account between our read and our write.

When STM Wins

SituationMutexSTM
Single shared value✓ simple✓ fine
Multiple related values✗ deadlock risk✓ composable
Read-heavy workloads✗ blocks writers✓ reads never block
Composing two existing operations✗ requires coordination✓ compose Stm values
Long operations with I/O✓ (STM would retry too much)✗ wrong tool

STM shines when:

  • You need to update multiple values atomically
  • You’re composing smaller transactional operations into larger ones
  • Contention is low (retries are cheap)

Avoid STM for long-running operations that do I/O. Transactions should be short and pure; use Effect for I/O and Stm for transactional state changes.