NAME
Acme::Parataxis::TVar - Software Transactional Memory (STM) with Versioned Cells
SYNOPSIS
use v5.40;
use Acme::Parataxis qw[async fiber await_sleep atomically retry];
use Acme::Parataxis::TVar;
my $account = Acme::Parataxis::TVar->new( value => 5 );
async {
# Consumer: waits for sufficient balance using retry
my $taker = fiber {
atomically {
my $have = $account->get;
retry if $have < 10; # balance insufficient: park until modified
$account->set( $have - 10 );
return 'withdrew 10';
};
};
await_sleep(10);
say "Taker parked; waiters: " . $account->waiters; # 1
# Producer: deposit funds to satisfy the condition
atomically {
$account->set( $account->get + 20 );
};
say $taker->await . ", balance: " . $account->value; # withdrew 10, balance: 15
};
DESCRIPTION
A TVar (Transactional Variable) is a mutable cell paired with a version counter. Rather than reading and writing cells directly, operations are wrapped in an atomically block and accessed through get and set. Writes remain staged and isolated until the outermost transaction commits; at commit time, all writes land atomically as a single unit, or none do.
This provides lock-free composability:
Zero interleaving: Two fibers performing multi-variable transfers cannot observe half-completed states. Either the entire transaction commits, or it conflicts and restarts.
Automatic conflict resolution: If another fiber commits changes to any TVar read by your transaction before your transaction commits, your staged writes are discarded and your block is automatically re-run from the top.
Conditional waiting without polling: Calling
retryaborts the current transaction and parks the calling fiber until at least one of the TVars in its read set is committed by another fiber, whereupon the block re-runs automatically.
Critical Side-Effect Rule: Because an atomically block can be re-run multiple times due to commit conflicts or retry calls, the block must be free of irreversible side effects. Do not perform network I/O, file writes, channel sends, or logging inside an atomically block. Perform pure calculations inside the transaction, and execute side effects afterward from the committed state.
METHODS
new( value => $initial )
my $count = Acme::Parataxis::TVar->new( value => 0 );
my $text = Acme::Parataxis::TVar->new( value => 'initial' );
Constructs a new TVar cell holding $initial. The value parameter is required. A newly created cell starts at version 0.
get( )
atomically {
my $current = $cell->get;
};
Reads the current value and journals the read in the active transaction log.
Read-your-writes: If
set()was called earlier in the same transaction,get()returns that staged value.Journaling: Records the cell's version at the time of the read. If that version has changed by commit time, the transaction aborts and restarts.
Must be called from within an atomically block (croaks otherwise).
set( $value )
atomically {
$cell->set($new_value);
};
Stages $value as the cell's pending value for this transaction and returns that value. Staged writes are completely invisible to other fibers and to "value( )" until the transaction commits successfully. Falsy values (0, '', and undef) are supported.
Must be called from within an atomically block (croaks otherwise).
value( )
my $val = $cell->value;
Returns the committed value of the cell directly. Can be called from anywhere (no fiber or transaction required).
Warning: value() is an unjournaled read intended strictly for tests, logging, and diagnostics. Reading a cell via value() inside a transaction bypasses conflict detection and can lead to lost updates (see "Behavior Notes").
version( )
Returns the cell's commit counter (incremented every time a transaction successfully writes to this cell). Can be read from outside a transaction.
waiters( )
Returns the number of fibers currently parked in a retry call whose read set includes this TVar.
TRANSACTIONS
atomically and retry are exported by Acme::Parataxis.
atomically( sub { ... } )
atomically {
my $a_val = $a->get;
my $b_val = $b->get;
$a->set( $a_val - 10 );
$b->set( $b_val + 10 );
};
Executes the code block as an atomic transaction on the current fiber. Nested atomically blocks join the enclosing transaction; staged writes from inner blocks commit only when the outermost transaction completes. Returns the block's return value in the caller's context.
retry( )
atomically {
my $val = $cell->get;
retry if $val <= 0;
$cell->set( $val - 1 );
};
Aborts the transaction, discards staged writes, and parks the fiber until one or more TVars in its read set change. Once modified, the fiber is woken and the transaction re-executes from the beginning. Must be called inside atomically and after reading at least one TVar (croaks if called with an empty read set).
Recipes
Deadlock-free account transfers
sub transfer ($from, $to, $amount) {
atomically {
my $bal_from = $from->get;
retry if $bal_from < $amount; # wait for funds
my $bal_to = $to->get;
$from->set( $bal_from - $amount );
$to->set( $bal_to + $amount );
};
}
No locks are held during computation. If two fibers concurrently transfer between the same accounts in opposite directions, one commits and the other restarts cleanly without deadlocking.
Behavior Notes
Semicolons after
atomically { ... }Because
atomicallyhas a(&)prototype, omitting the trailing semicolon on anatomically { ... };block statement causes Perl to parse subsequent lines as additional arguments, producing confusingToo many argumentscompile errors.value()vsget()Calling
$cell->valueinside anatomicallyblock reads the raw committed memory without logging it in the transaction's read set. If another fiber modifies the cell before you commit, your transaction will not detect the conflict, silently overwriting the other fiber's work. Always use$cell->getinside transactions.Nested
retryunwinds the outer blockCalling
retryinside a nestedatomicallyblock aborts the entire outermost transaction, not just the inner block. Execution restarts from the top of the outermost block.
SEE ALSO
Acme::Parataxis, Acme::Parataxis::Sync::Mutex, Acme::Parataxis::Sync::RwLock
AUTHOR
Sanko Robinson https://github.com/sanko
LICENSE
Copyright (C) Sanko Robinson.
This library is free software; you can redistribute it and/or modify it under the terms found in the Artistic License 2.