JS - Concurrency
· 3 min read
Imagine you have a bank account and 10 transactions arrive at the same time — some deposit money, some withdraw it.
How do you ensure that all transactions are processed correctly without overwriting each other?
let accountBalance = 100;
// Simulating Concurrent Transactions
async function runTransactions() {
console.log(`Starting Balance: ${accountBalance}`);
await Promise.all([
addAmount(50), // Should be 150
addAmount(-30), // Should be 120
addAmount(20), // Should be 140
addAmount(-10), // Should be 130
addAmount(40) // Should be 170 --> Only this returns final output
]);
console.log(`Final Balance: ${accountBalance}`);
// Expected: 170, Actual: 140 as all transactions start with 100 balance
}
async function addAmount(amount) {
const balance = await getBalance();
const newBalance = balance + amount;
console.log(
"Read Balance:", balance,
"| Adding:", amount,
"| New Balance:", newBalance
);
await saveBalance(newBalance);
}
async function getBalance() {
return accountBalance;
}
async function saveBalance(newBalance){
accountBalance = newBalance;
}
runTransactions();
Read Balance: 100 | Adding: 50 | New Balance: 150
Read Balance: 100 | Adding: -30 | New Balance: 70
Read Balance: 100 | Adding: 20 | New Balance: 120
Read Balance: 100 | Adding: -10 | New Balance: 90
Read Balance: 100 | Adding: 40 | New Balance: 140
Final Balance: 140
- Each transaction reads 100 instead of the updated balance.
- Transactions compute their own new balance separately:
- Last transaction (
+40
) overwrites previous updates.
Challenges
How do we prevent multiple operations from interfering with each other?
- Locks? JavaScript doesn’t have built-in locks.
- Sequential execution? But we still want concurrency.
- Atomic updates? Possible, but how?
Solution: Amotic Updates using MUTEX
A Mutex (Mutual Exclusion) ensures that only one operation modifies the balance at a time.
function createMutex() {
let locked = false;
const queue = [];
return {
lock: function() {
// Each lock call returns a promise
// the caller of lock, awaits for this to resolve
return new Promise((resolve) => {
if(!locked) {
// No one holds the lock yet - take it
locked = true;
resolve();
} else {
// lock is taken - queue up the resolver
queue.push(resolve);
}
});
},
unlock: function() {
if(queue.length > 0) {
// hand off the lock to the next waiting caller
const nextResolve = queue.shift();
nextResolve();
} else {
// no one is waiting - release the lock
locked = false;
}
}
}
}
const mutex = createMutex();
// ✅ Mutex-Protected Function
async function addAmountConcurrent(amount) {
await mutex.lock();
const balance = await getBalance();
const newBalance = balance + amount;
console.log(
"💰 Read Balance:", balance,
"| Adding:", amount,
"| New Balance:", newBalance
);
await saveBalance(newBalance)
mutex.unlock();
}
let accountBalance = 100;
async function getBalance() {
return accountBalance;
}
async function saveBalance(newBalance) {
accountBalance = newBalance;
}
async function runTransactionsMutex() {
console.log(`Starting Balance: ${accountBalance}`);
await Promise.all([
addAmountConcurrent(50),
addAmountConcurrent(-30),
addAmountConcurrent(20),
addAmountConcurrent(-10),
addAmountConcurrent(40)
]);
console.log(`💰 Final Balance: ${accountBalance}`);
}
Fixed final output
runTransactionsMutex();
💰 Read Balance: 100 | Adding: 50 | New Balance: 150
💰 Read Balance: 150 | Adding: -30 | New Balance: 120
💰 Read Balance: 120 | Adding: 20 | New Balance: 140
💰 Read Balance: 140 | Adding: -10 | New Balance: 130
💰 Read Balance: 130 | Adding: 40 | New Balance: 170
💰 Final Balance: 170