├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── JavaScript ├── 1-mutex.js ├── 2-deadlock.js ├── 3-livelock.js ├── 4-race-condition.js ├── 5-no-race.js ├── 6-blocking.js ├── 7-spin-lock.js ├── 8-async.js └── 9-web-locks.js ├── LICENSE └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": "latest" 10 | }, 11 | "globals": { 12 | "BigInt": true, 13 | "SharedArrayBuffer": true, 14 | "Atomics": true 15 | }, 16 | "rules": { 17 | "indent": [ 18 | "error", 19 | 2 20 | ], 21 | "linebreak-style": [ 22 | "error", 23 | "unix" 24 | ], 25 | "quotes": [ 26 | "error", 27 | "single" 28 | ], 29 | "semi": [ 30 | "error", 31 | "always" 32 | ], 33 | "no-loop-func": [ 34 | "error" 35 | ], 36 | "block-spacing": [ 37 | "error", 38 | "always" 39 | ], 40 | "camelcase": [ 41 | "error" 42 | ], 43 | "eqeqeq": [ 44 | "error", 45 | "always" 46 | ], 47 | "strict": [ 48 | "error", 49 | "global" 50 | ], 51 | "brace-style": [ 52 | "error", 53 | "1tbs", 54 | { 55 | "allowSingleLine": true 56 | } 57 | ], 58 | "comma-style": [ 59 | "error", 60 | "last" 61 | ], 62 | "comma-spacing": [ 63 | "error", 64 | { 65 | "before": false, 66 | "after": true 67 | } 68 | ], 69 | "eol-last": [ 70 | "error" 71 | ], 72 | "func-call-spacing": [ 73 | "error", 74 | "never" 75 | ], 76 | "key-spacing": [ 77 | "error", 78 | { 79 | "beforeColon": false, 80 | "afterColon": true, 81 | "mode": "minimum" 82 | } 83 | ], 84 | "keyword-spacing": [ 85 | "error", 86 | { 87 | "before": true, 88 | "after": true, 89 | "overrides": { 90 | "function": { 91 | "after": false 92 | } 93 | } 94 | } 95 | ], 96 | "max-len": [ 97 | "error", 98 | { 99 | "code": 80, 100 | "ignoreUrls": true 101 | } 102 | ], 103 | "max-nested-callbacks": [ 104 | "error", 105 | { 106 | "max": 7 107 | } 108 | ], 109 | "new-cap": [ 110 | "error", 111 | { 112 | "newIsCap": true, 113 | "capIsNew": false, 114 | "properties": true 115 | } 116 | ], 117 | "new-parens": [ 118 | "error" 119 | ], 120 | "no-lonely-if": [ 121 | "error" 122 | ], 123 | "no-trailing-spaces": [ 124 | "error" 125 | ], 126 | "no-unneeded-ternary": [ 127 | "error" 128 | ], 129 | "no-whitespace-before-property": [ 130 | "error" 131 | ], 132 | "object-curly-spacing": [ 133 | "error", 134 | "always" 135 | ], 136 | "operator-assignment": [ 137 | "error", 138 | "always" 139 | ], 140 | "operator-linebreak": [ 141 | "error", 142 | "after" 143 | ], 144 | "semi-spacing": [ 145 | "error", 146 | { 147 | "before": false, 148 | "after": true 149 | } 150 | ], 151 | "space-before-blocks": [ 152 | "error", 153 | "always" 154 | ], 155 | "space-before-function-paren": [ 156 | "error", 157 | { 158 | "anonymous": "never", 159 | "named": "never", 160 | "asyncArrow": "always" 161 | } 162 | ], 163 | "space-in-parens": [ 164 | "error", 165 | "never" 166 | ], 167 | "space-infix-ops": [ 168 | "error" 169 | ], 170 | "space-unary-ops": [ 171 | "error", 172 | { 173 | "words": true, 174 | "nonwords": false, 175 | "overrides": { 176 | "typeof": false 177 | } 178 | } 179 | ], 180 | "no-unreachable": [ 181 | "error" 182 | ], 183 | "no-global-assign": [ 184 | "error" 185 | ], 186 | "no-self-compare": [ 187 | "error" 188 | ], 189 | "no-unmodified-loop-condition": [ 190 | "error" 191 | ], 192 | "no-constant-condition": [ 193 | "error", 194 | { 195 | "checkLoops": false 196 | } 197 | ], 198 | "no-console": [ 199 | "off" 200 | ], 201 | "no-useless-concat": [ 202 | "error" 203 | ], 204 | "no-useless-escape": [ 205 | "error" 206 | ], 207 | "no-shadow-restricted-names": [ 208 | "error" 209 | ], 210 | "no-use-before-define": [ 211 | "error", 212 | { 213 | "functions": false 214 | } 215 | ], 216 | "arrow-parens": [ 217 | "error", 218 | "always" 219 | ], 220 | "arrow-body-style": [ 221 | "error", 222 | "as-needed" 223 | ], 224 | "arrow-spacing": [ 225 | "error" 226 | ], 227 | "no-confusing-arrow": [ 228 | "error", 229 | { 230 | "allowParens": true 231 | } 232 | ], 233 | "no-useless-computed-key": [ 234 | "error" 235 | ], 236 | "no-useless-rename": [ 237 | "error" 238 | ], 239 | "no-var": [ 240 | "error" 241 | ], 242 | "object-shorthand": [ 243 | "error", 244 | "always" 245 | ], 246 | "prefer-arrow-callback": [ 247 | "error" 248 | ], 249 | "prefer-const": [ 250 | "error" 251 | ], 252 | "prefer-numeric-literals": [ 253 | "error" 254 | ], 255 | "prefer-rest-params": [ 256 | "error" 257 | ], 258 | "prefer-spread": [ 259 | "error" 260 | ], 261 | "rest-spread-spacing": [ 262 | "error", 263 | "never" 264 | ], 265 | "template-curly-spacing": [ 266 | "error", 267 | "never" 268 | ], 269 | "consistent-return": [ 270 | "error", 271 | { "treatUndefinedAsUnspecified": true } 272 | ] 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /JavaScript/1-mutex.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | const LOCKED = 0; 7 | const UNLOCKED = 1; 8 | 9 | class Mutex { 10 | constructor(shared, offset = 0, initial = false) { 11 | this.lock = new Int32Array(shared, offset, 1); 12 | if (initial) Atomics.store(this.lock, 0, UNLOCKED); 13 | this.owner = false; 14 | } 15 | 16 | enter(callback) { 17 | Atomics.wait(this.lock, 0, LOCKED); 18 | Atomics.store(this.lock, 0, LOCKED); 19 | this.owner = true; 20 | setTimeout(callback, 0); 21 | } 22 | 23 | leave() { 24 | if (!this.owner) return false; 25 | Atomics.store(this.lock, 0, UNLOCKED); 26 | Atomics.notify(this.lock, 0, 1); 27 | this.owner = false; 28 | return true; 29 | } 30 | } 31 | 32 | // Usage 33 | 34 | if (isMainThread) { 35 | const buffer = new SharedArrayBuffer(4); 36 | const mutex = new Mutex(buffer, 0, true); 37 | console.dir({ mutex }); 38 | new Worker(__filename, { workerData: buffer }); 39 | new Worker(__filename, { workerData: buffer }); 40 | } else { 41 | const { threadId, workerData } = threads; 42 | const mutex = new Mutex(workerData); 43 | if (threadId === 1) { 44 | mutex.enter(() => { 45 | console.log('Entered mutex'); 46 | setTimeout(() => { 47 | if (mutex.leave()) { 48 | console.log('Left mutex'); 49 | } 50 | }, 100); 51 | }); 52 | } else if (!mutex.leave()) { 53 | console.log('Can not leave mutex: not owner'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /JavaScript/2-deadlock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | const LOCKED = 0; 7 | const UNLOCKED = 1; 8 | 9 | class Mutex { 10 | constructor(shared, offset = 0, initial = false) { 11 | this.lock = new Int32Array(shared, offset, 1); 12 | if (initial) Atomics.store(this.lock, 0, UNLOCKED); 13 | this.owner = false; 14 | } 15 | 16 | enter(callback) { 17 | Atomics.wait(this.lock, 0, LOCKED); 18 | Atomics.store(this.lock, 0, LOCKED); 19 | this.owner = true; 20 | setTimeout(callback, 0); 21 | } 22 | 23 | leave() { 24 | if (!this.owner) return; 25 | Atomics.store(this.lock, 0, UNLOCKED); 26 | Atomics.notify(this.lock, 0, 1); 27 | this.owner = false; 28 | } 29 | } 30 | 31 | // Usage 32 | 33 | if (isMainThread) { 34 | const buffer = new SharedArrayBuffer(8); 35 | const mutex1 = new Mutex(buffer, 0, true); 36 | const mutex2 = new Mutex(buffer, 4, true); 37 | console.dir({ mutex1, mutex2 }); 38 | new Worker(__filename, { workerData: buffer }); 39 | new Worker(__filename, { workerData: buffer }); 40 | } else { 41 | const { threadId, workerData } = threads; 42 | const mutex1 = new Mutex(workerData); 43 | const mutex2 = new Mutex(workerData, 4); 44 | 45 | if (threadId === 1) { 46 | mutex1.enter(() => { 47 | console.log('Entered mutex1 from worker1'); 48 | setTimeout(() => { 49 | mutex2.enter(() => { 50 | console.log('Entered mutex2 from worker1'); 51 | if (mutex1.leave()) console.log('Left mutex1 from worker1'); 52 | if (mutex2.leave()) console.log('Left mutex2 from worker1'); 53 | }); 54 | }, 100); 55 | }); 56 | } else { 57 | mutex2.enter(() => { 58 | console.log('Entered mutex2 from worker2'); 59 | // Uncomment to fix deadlock 60 | // if (mutex2.leave()) console.log('Left mutex2 from worker2'); 61 | setTimeout(() => { 62 | mutex1.enter(() => { 63 | console.log('Entered mutex1 from worker2'); 64 | if (mutex2.leave()) console.log('Left mutex2 from worker2'); 65 | if (mutex1.leave()) console.log('Left mutex1 from worker2'); 66 | }); 67 | }, 100); 68 | }); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /JavaScript/3-livelock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | const LOCKED = 0; 7 | const UNLOCKED = 1; 8 | 9 | class Mutex { 10 | constructor(shared, offset = 0, initial = false) { 11 | this.lock = new Int32Array(shared, offset, 1); 12 | if (initial) Atomics.store(this.lock, 0, UNLOCKED); 13 | this.owner = false; 14 | } 15 | 16 | enter(callback) { 17 | console.log(`Wait in worker${threads.threadId}`); 18 | Atomics.wait(this.lock, 0, LOCKED); 19 | console.log(`endWait in worker${threads.threadId}`); 20 | Atomics.store(this.lock, 0, LOCKED); 21 | this.owner = true; 22 | setTimeout(callback, 0); 23 | } 24 | 25 | leave() { 26 | if (!this.owner) return; 27 | Atomics.store(this.lock, 0, UNLOCKED); 28 | Atomics.notify(this.lock, 0, 1); 29 | this.owner = false; 30 | } 31 | } 32 | 33 | // Usage 34 | 35 | if (isMainThread) { 36 | const buffer = new SharedArrayBuffer(8); 37 | const mutex1 = new Mutex(buffer, 0, true); 38 | const mutex2 = new Mutex(buffer, 4, true); 39 | console.dir({ mutex1, mutex2 }); 40 | new Worker(__filename, { workerData: buffer }); 41 | new Worker(__filename, { workerData: buffer }); 42 | } else { 43 | const { threadId, workerData } = threads; 44 | const mutex1 = new Mutex(workerData); 45 | const mutex2 = new Mutex(workerData, 4); 46 | 47 | const loop = () => { 48 | mutex1.enter(() => { 49 | console.log(`Entered mutex1 from worker${threadId}`); 50 | if (mutex1.leave()) console.log(`Left mutex1 from worker${threadId}`); 51 | }); 52 | mutex2.enter(() => { 53 | console.log(`Entered mutex2 from worker${threadId}`); 54 | if (mutex2.leave()) console.log(`Left mutex2 from worker${threadId}`); 55 | }); 56 | setTimeout(loop, 0); 57 | }; 58 | loop(); 59 | 60 | } 61 | -------------------------------------------------------------------------------- /JavaScript/4-race-condition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | class Point { 7 | constructor(data, x, y) { 8 | this.data = data; 9 | if (typeof x === 'number') data[0] = x; 10 | if (typeof y === 'number') data[1] = y; 11 | } 12 | 13 | get x() { 14 | return this.data[0]; 15 | } 16 | set x(value) { 17 | this.data[0] = value; 18 | } 19 | 20 | get y() { 21 | return this.data[1]; 22 | } 23 | set y(value) { 24 | this.data[1] = value; 25 | } 26 | 27 | move(x, y) { 28 | this.x += x; 29 | this.y += y; 30 | } 31 | } 32 | 33 | // Usage 34 | 35 | if (isMainThread) { 36 | const buffer = new SharedArrayBuffer(4); 37 | const array = new Int8Array(buffer, 0, 2); 38 | const point = new Point(array, 0, 0); 39 | console.dir({ buffer, point }); 40 | new Worker(__filename, { workerData: buffer }); 41 | new Worker(__filename, { workerData: buffer }); 42 | } else { 43 | const { threadId, workerData } = threads; 44 | const array = new Int8Array(workerData, 0, 2); 45 | const point = new Point(array); 46 | if (threadId === 1) { 47 | for (let i = 0; i < 1000000; i++) { 48 | point.move(1, 1); 49 | } 50 | } else { 51 | for (let i = 0; i < 1000000; i++) { 52 | point.move(-1, -1); 53 | } 54 | } 55 | console.dir({ point }); 56 | } 57 | -------------------------------------------------------------------------------- /JavaScript/5-no-race.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | const LOCKED = 0; 7 | const UNLOCKED = 1; 8 | 9 | class Mutex { 10 | constructor(shared, offset = 0, initial = false) { 11 | this.lock = new Int32Array(shared, offset, 1); 12 | if (initial) Atomics.store(this.lock, 0, UNLOCKED); 13 | this.owner = false; 14 | } 15 | 16 | enter() { 17 | let prev = Atomics.exchange(this.lock, 0, LOCKED); 18 | while (prev !== UNLOCKED) { 19 | Atomics.wait(this.lock, 0, LOCKED); 20 | prev = Atomics.exchange(this.lock, 0, LOCKED); 21 | } 22 | this.owner = true; 23 | } 24 | 25 | leave() { 26 | if (!this.owner) return; 27 | Atomics.store(this.lock, 0, UNLOCKED); 28 | Atomics.notify(this.lock, 0, 1); 29 | this.owner = false; 30 | } 31 | } 32 | 33 | class Point { 34 | constructor(data, x, y) { 35 | this.data = data; 36 | if (typeof x === 'number') data[0] = x; 37 | if (typeof y === 'number') data[1] = y; 38 | } 39 | 40 | get x() { 41 | return this.data[0]; 42 | } 43 | set x(value) { 44 | this.data[0] = value; 45 | } 46 | 47 | get y() { 48 | return this.data[1]; 49 | } 50 | set y(value) { 51 | this.data[1] = value; 52 | } 53 | 54 | move(x, y) { 55 | this.x += x; 56 | this.y += y; 57 | } 58 | } 59 | 60 | // Usage 61 | 62 | if (isMainThread) { 63 | const buffer = new SharedArrayBuffer(12); 64 | const mutex = new Mutex(buffer, 0, true); 65 | const array = new Int32Array(buffer, 4, 2); 66 | const point = new Point(array, 0, 0); 67 | console.dir({ mutex, point }); 68 | new Worker(__filename, { workerData: buffer }); 69 | new Worker(__filename, { workerData: buffer }); 70 | } else { 71 | const { threadId, workerData } = threads; 72 | const mutex = new Mutex(workerData); 73 | const array = new Int32Array(workerData, 4, 2); 74 | const point = new Point(array); 75 | if (threadId === 1) { 76 | for (let i = 0; i < 1000000; i++) { 77 | mutex.enter(); 78 | point.move(1, 1); 79 | mutex.leave(); 80 | } 81 | mutex.enter(); 82 | console.dir({ point }); 83 | mutex.leave(); 84 | } else { 85 | for (let i = 0; i < 1000000; i++) { 86 | mutex.enter(); 87 | point.move(-1, -1); 88 | mutex.leave(); 89 | } 90 | mutex.enter(); 91 | console.dir({ point }); 92 | mutex.leave(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /JavaScript/6-blocking.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | const LOCKED = 0; 7 | const UNLOCKED = 1; 8 | 9 | class Mutex { 10 | constructor(shared, offset = 0, initial = false) { 11 | this.lock = new Int32Array(shared, offset, 1); 12 | if (initial) Atomics.store(this.lock, 0, UNLOCKED); 13 | this.owner = false; 14 | } 15 | 16 | enter() { 17 | let prev = Atomics.exchange(this.lock, 0, LOCKED); 18 | while (prev !== UNLOCKED) { 19 | Atomics.wait(this.lock, 0, LOCKED); 20 | prev = Atomics.exchange(this.lock, 0, LOCKED); 21 | } 22 | this.owner = true; 23 | } 24 | 25 | leave() { 26 | if (!this.owner) return; 27 | Atomics.store(this.lock, 0, UNLOCKED); 28 | Atomics.notify(this.lock, 0, 1); 29 | this.owner = false; 30 | } 31 | } 32 | 33 | // Usage 34 | 35 | if (isMainThread) { 36 | const buffer = new SharedArrayBuffer(4); 37 | const mutex = new Mutex(buffer, 0, true); 38 | console.dir({ mutex }); 39 | new Worker(__filename, { workerData: buffer }); 40 | new Worker(__filename, { workerData: buffer }); 41 | } else { 42 | const { threadId, workerData } = threads; 43 | const mutex = new Mutex(workerData); 44 | 45 | setInterval(() => { 46 | console.log(`Interval ${threadId}`); 47 | }, 500); 48 | 49 | const loop = () => { 50 | mutex.enter(); 51 | console.log(`Enter ${threadId}`); 52 | setTimeout(() => { 53 | mutex.leave(); 54 | console.log(`Leave ${threadId}`); 55 | setTimeout(loop, 0); 56 | }, 5000); 57 | }; 58 | loop(); 59 | } 60 | -------------------------------------------------------------------------------- /JavaScript/7-spin-lock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | const LOCKED = 0; 7 | const UNLOCKED = 1; 8 | 9 | class Mutex { 10 | constructor(shared, offset = 0, initial = false) { 11 | this.lock = new Int32Array(shared, offset, 1); 12 | if (initial) Atomics.store(this.lock, 0, UNLOCKED); 13 | this.owner = false; 14 | } 15 | 16 | enter() { 17 | return new Promise((resolve) => { 18 | const tryEnter = () => { 19 | const prev = Atomics.exchange(this.lock, 0, LOCKED); 20 | if (prev === UNLOCKED) { 21 | this.owner = true; 22 | resolve(); 23 | } else { 24 | setTimeout(tryEnter, 0); 25 | } 26 | }; 27 | tryEnter(); 28 | }); 29 | } 30 | 31 | leave() { 32 | if (!this.owner) return; 33 | Atomics.store(this.lock, 0, UNLOCKED); 34 | this.owner = false; 35 | } 36 | } 37 | 38 | // Usage 39 | 40 | if (isMainThread) { 41 | const buffer = new SharedArrayBuffer(4); 42 | const mutex = new Mutex(buffer, 0, true); 43 | console.dir({ mutex }); 44 | new Worker(__filename, { workerData: buffer }); 45 | new Worker(__filename, { workerData: buffer }); 46 | } else { 47 | const { threadId, workerData } = threads; 48 | const mutex = new Mutex(workerData); 49 | 50 | setInterval(() => { 51 | console.log(`Interval ${threadId}`); 52 | }, 500); 53 | 54 | const loop = async () => { 55 | await mutex.enter(); 56 | console.log(`Enter ${threadId}`); 57 | setTimeout(() => { 58 | mutex.leave(); 59 | console.log(`Leave ${threadId}`); 60 | setTimeout(loop, 0); 61 | }, 5000); 62 | }; 63 | loop(); 64 | } 65 | -------------------------------------------------------------------------------- /JavaScript/8-async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | const LOCKED = 0; 7 | const UNLOCKED = 1; 8 | 9 | class Mutex { 10 | constructor(messagePort, shared, offset = 0, initial = false) { 11 | this.port = messagePort; 12 | this.lock = new Int32Array(shared, offset, 1); 13 | if (initial) Atomics.store(this.lock, 0, UNLOCKED); 14 | this.owner = false; 15 | this.trying = false; 16 | this.resolve = null; 17 | if (messagePort) { 18 | messagePort.on('message', (kind) => { 19 | if (kind === 'leave' && this.trying) this.tryEnter(); 20 | }); 21 | } 22 | } 23 | 24 | enter() { 25 | return new Promise((resolve) => { 26 | this.resolve = resolve; 27 | this.trying = true; 28 | this.tryEnter(); 29 | }); 30 | } 31 | 32 | tryEnter() { 33 | if (!this.resolve) return; 34 | const prev = Atomics.exchange(this.lock, 0, LOCKED); 35 | if (prev === UNLOCKED) { 36 | this.owner = true; 37 | this.trying = false; 38 | this.resolve(); 39 | this.resolve = null; 40 | } 41 | } 42 | 43 | leave() { 44 | if (!this.owner) return; 45 | Atomics.store(this.lock, 0, UNLOCKED); 46 | this.port.postMessage('leave'); 47 | this.owner = false; 48 | } 49 | } 50 | 51 | class Thread { 52 | constructor(data) { 53 | const worker = new Worker(__filename, { workerData: data }); 54 | this.worker = worker; 55 | Thread.workers.add(worker); 56 | worker.on('message', (kind) => { 57 | for (const next of Thread.workers) { 58 | if (next !== worker) { 59 | next.postMessage(kind); 60 | } 61 | } 62 | }); 63 | } 64 | } 65 | 66 | Thread.workers = new Set(); 67 | 68 | // Usage 69 | 70 | if (isMainThread) { 71 | const buffer = new SharedArrayBuffer(4); 72 | const mutex = new Mutex(null, buffer, 0, true); 73 | console.dir({ mutex }); 74 | new Thread(buffer); 75 | new Thread(buffer); 76 | } else { 77 | const { threadId, workerData, parentPort } = threads; 78 | const mutex = new Mutex(parentPort, workerData); 79 | 80 | setInterval(() => { 81 | console.log(`Interval ${threadId}`); 82 | }, 500); 83 | 84 | const loop = async () => { 85 | await mutex.enter(); 86 | console.log(`Enter ${threadId}`); 87 | setTimeout(() => { 88 | mutex.leave(); 89 | console.log(`Leave ${threadId}`); 90 | setTimeout(loop, 0); 91 | }, 5000); 92 | }; 93 | loop(); 94 | } 95 | -------------------------------------------------------------------------------- /JavaScript/9-web-locks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | Worker, isMainThread, threadId, parentPort 5 | } = require('node:worker_threads'); 6 | 7 | const threads = new Set(); 8 | 9 | const LOCKED = 0; 10 | const UNLOCKED = 1; 11 | 12 | const locks = { 13 | resources: new Map() 14 | }; 15 | 16 | class Mutex { 17 | constructor(resourceName, shared, initial = false) { 18 | this.resourceName = resourceName; 19 | this.lock = new Int32Array(shared, 0, 1); 20 | if (initial) Atomics.store(this.lock, 0, UNLOCKED); 21 | this.owner = false; 22 | this.trying = false; 23 | this.callback = null; 24 | } 25 | 26 | async enter(callback) { 27 | this.callback = callback; 28 | this.trying = true; 29 | await this.tryEnter(); 30 | } 31 | 32 | async tryEnter() { 33 | if (!this.callback) return; 34 | const prev = Atomics.exchange(this.lock, 0, LOCKED); 35 | if (prev === UNLOCKED) { 36 | this.owner = true; 37 | this.trying = false; 38 | await this.callback(this); 39 | this.callback = null; 40 | this.leave(); 41 | } 42 | } 43 | 44 | leave() { 45 | if (!this.owner) return; 46 | Atomics.store(this.lock, 0, UNLOCKED); 47 | this.owner = false; 48 | locks.sendMessage({ kind: 'leave', resourceName: this.resourceName }); 49 | } 50 | } 51 | 52 | locks.request = async (resourceName, callback) => { 53 | let lock = locks.resources.get(resourceName); 54 | if (!lock) { 55 | const buffer = new SharedArrayBuffer(4); 56 | lock = new Mutex(resourceName, buffer, true); 57 | locks.resources.set(resourceName, lock); 58 | locks.sendMessage({ kind: 'create', resourceName, buffer }); 59 | } 60 | await lock.enter(callback); 61 | }; 62 | 63 | locks.sendMessage = (message) => { 64 | if (isMainThread) { 65 | for (const thread of threads) { 66 | thread.worker.postMessage(message); 67 | } 68 | } else { 69 | parentPort.postMessage(message); 70 | } 71 | }; 72 | 73 | locks.receiveMessage = (message) => { 74 | const { kind, resourceName, buffer } = message; 75 | if (kind === 'create') { 76 | const lock = new Mutex(resourceName, buffer); 77 | locks.resources.set(resourceName, lock); 78 | } else if (kind === 'leave') { 79 | for (const mutex of locks.resources) { 80 | if (mutex.trying) mutex.tryEnter(); 81 | } 82 | } 83 | }; 84 | 85 | if (!isMainThread) { 86 | parentPort.on('message', locks.receiveMessage); 87 | } 88 | 89 | class Thread { 90 | constructor() { 91 | const worker = new Worker(__filename); 92 | this.worker = worker; 93 | threads.add(this); 94 | worker.on('message', (message) => { 95 | for (const thread of threads) { 96 | if (thread.worker !== worker) { 97 | thread.worker.postMessage(message); 98 | } 99 | } 100 | locks.receiveMessage(message); 101 | }); 102 | } 103 | } 104 | 105 | // Usage 106 | 107 | if (isMainThread) { 108 | 109 | new Thread(); 110 | new Thread(); 111 | setTimeout(() => { 112 | process.exit(0); 113 | }, 300); 114 | 115 | } else { 116 | 117 | locks.request('A', async (lock) => { 118 | console.log(`Enter ${lock.resourceName} in ${threadId}`); 119 | }); 120 | 121 | setTimeout(async () => { 122 | await locks.request('B', async (lock) => { 123 | console.log(`Enter ${lock.resourceName} in ${threadId}`); 124 | }); 125 | console.log(`Leave all in ${threadId}`); 126 | }, 100); 127 | 128 | } 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2023 How.Programming.Works contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Mutex: Preventing race conditions with Mutual Exclusion 2 | 3 | [![Семафоры и мьютексы в JavaScript и Node.js](https://img.youtube.com/vi/JNLrITevhRI/0.jpg)](https://www.youtube.com/watch?v=JNLrITevhRI) 4 | 5 | [![Shared memory and multithreading in Node.js](https://image.slidesharecdn.com/timur-jsfest19-190406113647/95/shared-memory-and-multithreading-in-nodejs-timur-shemsedinov-jsfest19-1-638.jpg)](https://www.slideshare.net/tshemsedinov/shared-memory-and-multithreading-in-nodejs-timur-shemsedinov-jsfest19) 6 | --------------------------------------------------------------------------------