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.
<!-- Two concurrent transactions compete to increment the shared counter -->
<div class="tm-panel">
<div class="counter-row">
<span class="label">Shared 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">Transaction A</div>
<div class="txn-log" id="log-a"></div>
</div>
<div class="txn-box" id="box-b">
<div class="txn-title">Transaction 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">Run transactions</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="hint-row">Both transactions read the same value and race to increment it. The one that commits first wins; the other detects the conflict, aborts, and retries from the updated value.</div>
</div>
/* Styles for the transactional memory demo */
* { 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; }
/* Transactional Memory demo — speculative increment with conflict detection */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Shared state (simulates a memory location visible to both threads) */
let sharedCounter = 0;
let totalRetries = 0;
let running = false;
/* DOM references */
const counterEl = document.getElementById('counter');
const logA = document.getElementById('log-a');
const logB = document.getElementById('log-b');
const boxA = document.getElementById('box-a');
const boxB = document.getElementById('box-b');
const statusEl = document.getElementById('status');
const btnRun = document.getElementById('btn-run');
const btnReset = document.getElementById('btn-reset');
/* Tiny async delay to make steps visible */
const sleep = ms => new Promise(r => setTimeout(r, ms));
/* Append a log line to a transaction box */
function appendLog(el, text, cls) {
const line = document.createElement('div');
line.className = cls;
line.textContent = text;
el.appendChild(line);
}
/* Set the global status message */
function setStatus(text, cls) {
statusEl.textContent = text;
statusEl.className = 'status-row ' + cls;
}
/* Refresh the displayed counter value */
function renderCounter() {
counterEl.textContent = sharedCounter;
}
/* Run one transaction: read speculatively, detect conflict, abort/retry or commit */
async function runTransaction(name, logEl, boxEl, delay) {
let retries = 0;
while (true) {
/* Speculative read — snapshot the current shared value */
const readVal = sharedCounter;
logEl.innerHTML = '';
boxEl.className = 'txn-box committing';
appendLog(logEl, i18nFmt('READ counter = %v%', { v: readVal }), 'log-read');
await sleep(delay);
/* Compute the new value in the private buffer */
const newVal = readVal + 1;
appendLog(logEl, i18nFmt('WRITE counter ← %v%', { v: newVal }), 'log-write');
await sleep(60);
/* Conflict check — did another transaction modify the counter since we read it? */
if (sharedCounter !== readVal) {
/* Conflict detected: abort and retry */
retries++;
totalRetries++;
boxEl.className = 'txn-box aborting';
appendLog(logEl, i18nFmt('ABORT (conflict, retry #%r%)', { r: retries }), 'log-abort');
appendLog(logEl, '→ retrying...', 'log-retry');
await sleep(80);
continue;
}
/* No conflict: commit the new value to shared memory */
sharedCounter = newVal;
renderCounter();
boxEl.className = 'txn-box committed';
appendLog(logEl, i18nFmt('COMMIT counter = %v% ✓', { v: newVal }), 'log-commit');
return retries;
}
}
/* Entry point — launch both transactions concurrently */
async function runDemo() {
if (running) return;
running = true;
btnRun.disabled = true;
btnReset.disabled = true;
logA.innerHTML = '';
logB.innerHTML = '';
boxA.className = 'txn-box';
boxB.className = 'txn-box';
/* Both transactions start nearly simultaneously */
setStatus('Running transactions...', 'status-run');
/* A tiny stagger makes the conflict visible in the UI */
const [rA, rB] = await Promise.all([
runTransaction('A', logA, boxA, 0),
runTransaction('B', logB, boxB, 30)
]);
/* Show final result */
const total = rA + rB;
setStatus(
i18nFmt('Done — counter = %c% · %r% abort(s) total', { c: sharedCounter, r: total }),
'status-ok'
);
running = false;
btnRun.disabled = false;
btnReset.disabled = false;
}
/* Reset demo to initial state */
function resetDemo() {
if (running) return;
sharedCounter = 0;
totalRetries = 0;
logA.innerHTML = '';
logB.innerHTML = '';
boxA.className = 'txn-box';
boxB.className = 'txn-box';
renderCounter();
setStatus('', '');
}
btnRun.addEventListener('click', runDemo);
btnReset.addEventListener('click', resetDemo);
renderCounter();
<!-- Two concurrent transactions compete to increment the shared counter -->
<div class="tm-panel">
<div class="counter-row">
<span class="label">Shared 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">Transaction A</div>
<div class="txn-log" id="log-a"></div>
</div>
<div class="txn-box" id="box-b">
<div class="txn-title">Transaction 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">Run transactions</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="hint-row">Both transactions read the same value and race to increment it. The one that commits first wins; the other detects the conflict, aborts, and retries from the updated value.</div>
</div>
/* Styles for the transactional memory demo */
* { 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; }
/* Transactional Memory demo — speculative increment with conflict detection */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Shared state (simulates a memory location visible to both threads) */
let sharedCounter = 0;
let totalRetries = 0;
let running = false;
/* DOM references */
const counterEl = document.getElementById('counter');
const logA = document.getElementById('log-a');
const logB = document.getElementById('log-b');
const boxA = document.getElementById('box-a');
const boxB = document.getElementById('box-b');
const statusEl = document.getElementById('status');
const btnRun = document.getElementById('btn-run');
const btnReset = document.getElementById('btn-reset');
/* Tiny async delay to make steps visible */
const sleep = ms => new Promise(r => setTimeout(r, ms));
/* Append a log line to a transaction box */
function appendLog(el, text, cls) {
const line = document.createElement('div');
line.className = cls;
line.textContent = text;
el.appendChild(line);
}
/* Set the global status message */
function setStatus(text, cls) {
statusEl.textContent = text;
statusEl.className = 'status-row ' + cls;
}
/* Refresh the displayed counter value */
function renderCounter() {
counterEl.textContent = sharedCounter;
}
/* Run one transaction: read speculatively, detect conflict, abort/retry or commit */
async function runTransaction(name, logEl, boxEl, delay) {
let retries = 0;
while (true) {
/* Speculative read — snapshot the current shared value */
const readVal = sharedCounter;
logEl.innerHTML = '';
boxEl.className = 'txn-box committing';
appendLog(logEl, i18nFmt('READ counter = %v%', { v: readVal }), 'log-read');
await sleep(delay);
/* Compute the new value in the private buffer */
const newVal = readVal + 1;
appendLog(logEl, i18nFmt('WRITE counter ← %v%', { v: newVal }), 'log-write');
await sleep(60);
/* Conflict check — did another transaction modify the counter since we read it? */
if (sharedCounter !== readVal) {
/* Conflict detected: abort and retry */
retries++;
totalRetries++;
boxEl.className = 'txn-box aborting';
appendLog(logEl, i18nFmt('ABORT (conflict, retry #%r%)', { r: retries }), 'log-abort');
appendLog(logEl, '→ retrying...', 'log-retry');
await sleep(80);
continue;
}
/* No conflict: commit the new value to shared memory */
sharedCounter = newVal;
renderCounter();
boxEl.className = 'txn-box committed';
appendLog(logEl, i18nFmt('COMMIT counter = %v% ✓', { v: newVal }), 'log-commit');
return retries;
}
}
/* Entry point — launch both transactions concurrently */
async function runDemo() {
if (running) return;
running = true;
btnRun.disabled = true;
btnReset.disabled = true;
logA.innerHTML = '';
logB.innerHTML = '';
boxA.className = 'txn-box';
boxB.className = 'txn-box';
/* Both transactions start nearly simultaneously */
setStatus('Running transactions...', 'status-run');
/* A tiny stagger makes the conflict visible in the UI */
const [rA, rB] = await Promise.all([
runTransaction('A', logA, boxA, 0),
runTransaction('B', logB, boxB, 30)
]);
/* Show final result */
const total = rA + rB;
setStatus(
i18nFmt('Done — counter = %c% · %r% abort(s) total', { c: sharedCounter, r: total }),
'status-ok'
);
running = false;
btnRun.disabled = false;
btnReset.disabled = false;
}
/* Reset demo to initial state */
function resetDemo() {
if (running) return;
sharedCounter = 0;
totalRetries = 0;
logA.innerHTML = '';
logB.innerHTML = '';
boxA.className = 'txn-box';
boxB.className = 'txn-box';
renderCounter();
setStatus('', '');
}
btnRun.addEventListener('click', runDemo);
btnReset.addEventListener('click', resetDemo);
renderCounter();
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.
Comments
Loading comments...