├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── JavaScript ├── 1-shared.js ├── 2-binary.js ├── 3-counting.js ├── 4-callback.js ├── 5-atomics.js ├── 6-counting-safe.js ├── 7-binary-safe.js └── 8-await-counting.js ├── LICENSE └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{*.js,*.mjs,*.ts,*.json,*.yml}] 11 | indent_size = 2 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.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-shared.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const threads = require('node:worker_threads'); 4 | const { Worker, isMainThread } = threads; 5 | 6 | if (isMainThread) { 7 | const buffer = new SharedArrayBuffer(10); 8 | new Worker(__filename, { workerData: buffer }); 9 | new Worker(__filename, { workerData: buffer }); 10 | } else { 11 | const { threadId, workerData } = threads; 12 | const array = new Int8Array(workerData); 13 | const value = threadId === 1 ? 1 : -1; 14 | setInterval(() => { 15 | for (let i = 0; i < 10; i++) { 16 | array[i] += value; 17 | } 18 | console.dir([ threadId, array ]); 19 | }, 100); // change to 10 to see race condition 20 | } 21 | -------------------------------------------------------------------------------- /JavaScript/2-binary.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 BinarySemaphore { 10 | constructor(shared, offset = 0, init = false) { 11 | this.lock = new Int8Array(shared, offset, 1); 12 | if (init) this.lock[0] = UNLOCKED; 13 | } 14 | 15 | enter() { 16 | while (this.lock[0] !== UNLOCKED); 17 | this.lock[0] = LOCKED; 18 | } 19 | 20 | leave() { 21 | if (this.lock[0] === UNLOCKED) { 22 | throw new Error('Cannot leave unlocked BinarySemaphore'); 23 | } 24 | this.lock[0] = UNLOCKED; 25 | } 26 | } 27 | 28 | // Usage 29 | 30 | if (isMainThread) { 31 | const buffer = new SharedArrayBuffer(11); 32 | const semaphore = new BinarySemaphore(buffer, 0, true); 33 | console.dir({ semaphore }); 34 | new Worker(__filename, { workerData: buffer }); 35 | new Worker(__filename, { workerData: buffer }); 36 | } else { 37 | const { threadId, workerData } = threads; 38 | const semaphore = new BinarySemaphore(workerData); 39 | const array = new Int8Array(workerData, 1); 40 | const value = threadId === 1 ? 1 : -1; 41 | setInterval(() => { 42 | semaphore.enter(); 43 | for (let i = 0; i < 10; i++) { 44 | array[i] += value; 45 | } 46 | console.dir([ threadId, array ]); 47 | semaphore.leave(); 48 | }, 100); // change to 10 to see race condition 49 | } 50 | -------------------------------------------------------------------------------- /JavaScript/3-counting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('node:fs'); 4 | const threads = require('node:worker_threads'); 5 | const { Worker, isMainThread } = threads; 6 | 7 | class CountingSemaphore { 8 | constructor(shared, offset = 0, initial) { 9 | this.counter = new Int32Array(shared, offset, 1); 10 | if (typeof initial === 'number') { 11 | this.counter[0] = initial; 12 | } 13 | } 14 | 15 | enter() { 16 | while (this.counter[0] === 0); 17 | this.counter[0]--; 18 | } 19 | 20 | leave() { 21 | this.counter[0]++; 22 | } 23 | } 24 | 25 | // Usage 26 | 27 | if (isMainThread) { 28 | const buffer = new SharedArrayBuffer(4); 29 | // Try to change 10 to 2 at next line to see the problem 30 | const semaphore = new CountingSemaphore(buffer, 0, 10); 31 | console.dir({ semaphore: semaphore.counter[0] }); 32 | for (let i = 0; i < 20; i++) { 33 | new Worker(__filename, { workerData: buffer }); 34 | } 35 | } else { 36 | const { threadId, workerData } = threads; 37 | const semaphore = new CountingSemaphore(workerData); 38 | console.dir({ threadId, semaphore: semaphore.counter[0] }); 39 | const REPEAT_COUNT = 1000000; 40 | const file = `file-${threadId}.dat`; 41 | 42 | semaphore.enter(); 43 | const data = `Data from ${threadId}`.repeat(REPEAT_COUNT); 44 | fs.writeFile(file, data, () => { 45 | fs.unlink(file, () => { 46 | semaphore.leave(); 47 | }); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /JavaScript/4-callback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('node:fs'); 4 | const threads = require('node:worker_threads'); 5 | const { Worker, isMainThread } = threads; 6 | 7 | class CountingSemaphore { 8 | constructor(shared, offset = 0, initial) { 9 | this.counter = new Int32Array(shared, offset, 1); 10 | if (typeof initial === 'number') { 11 | this.counter[0] = initial; 12 | } 13 | this.queue = []; 14 | } 15 | 16 | enter(callback) { 17 | if (this.counter[0] > 0) { 18 | this.counter[0]--; 19 | setTimeout(callback, 0); 20 | } else { 21 | this.queue.push(callback); 22 | } 23 | } 24 | 25 | leave() { 26 | this.counter[0]++; 27 | if (this.queue.length > 0) { 28 | const callback = this.queue.shift(); 29 | this.counter[0]--; 30 | setTimeout(callback, 0); 31 | } 32 | } 33 | } 34 | 35 | // Usage 36 | 37 | if (isMainThread) { 38 | const buffer = new SharedArrayBuffer(4); 39 | // Try to change 10 to 2 at next line to check solution 40 | const semaphore = new CountingSemaphore(buffer, 0, 10); 41 | console.dir({ semaphore: semaphore.counter[0] }); 42 | for (let i = 0; i < 20; i++) { 43 | new Worker(__filename, { workerData: buffer }); 44 | } 45 | } else { 46 | const { threadId, workerData } = threads; 47 | const semaphore = new CountingSemaphore(workerData); 48 | console.dir({ threadId, semaphore: semaphore.counter[0] }); 49 | const REPEAT_COUNT = 1000000; 50 | const file = `file-${threadId}.dat`; 51 | 52 | semaphore.enter(() => { 53 | const data = `Data from ${threadId}`.repeat(REPEAT_COUNT); 54 | fs.writeFile(file, data, () => { 55 | fs.unlink(file, () => { 56 | semaphore.leave(); 57 | }); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /JavaScript/5-atomics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('node:fs'); 4 | const threads = require('node:worker_threads'); 5 | const { Worker, isMainThread } = threads; 6 | 7 | class CountingSemaphore { 8 | constructor(shared, offset = 0, initial) { 9 | this.counter = new Int32Array(shared, offset, 1); 10 | if (typeof initial === 'number') { 11 | Atomics.store(this.counter, 0, initial); 12 | } 13 | } 14 | 15 | enter(callback) { 16 | Atomics.wait(this.counter, 0, 0); 17 | Atomics.sub(this.counter, 0, 1); 18 | setTimeout(callback, 0); 19 | } 20 | 21 | leave() { 22 | Atomics.add(this.counter, 0, 1); 23 | Atomics.notify(this.counter, 0, 1); 24 | } 25 | } 26 | 27 | // Usage 28 | 29 | if (isMainThread) { 30 | const buffer = new SharedArrayBuffer(4); 31 | // Try to change 10 to 2 at next line to check solution 32 | const semaphore = new CountingSemaphore(buffer, 0, 10); 33 | console.dir({ semaphore: semaphore.counter[0] }); 34 | for (let i = 0; i < 20; i++) { 35 | new Worker(__filename, { workerData: buffer }); 36 | } 37 | } else { 38 | const { threadId, workerData } = threads; 39 | const semaphore = new CountingSemaphore(workerData); 40 | console.dir({ threadId, semaphore: semaphore.counter[0] }); 41 | const REPEAT_COUNT = 1000000; 42 | const file = `file-${threadId}.dat`; 43 | 44 | semaphore.enter(() => { 45 | const data = `Data from ${threadId}`.repeat(REPEAT_COUNT); 46 | fs.writeFile(file, data, () => { 47 | fs.unlink(file, () => { 48 | semaphore.leave(); 49 | }); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /JavaScript/6-counting-safe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('node:fs'); 4 | const threads = require('node:worker_threads'); 5 | const { Worker, isMainThread } = threads; 6 | 7 | class CountingSemaphore { 8 | constructor(shared, offset = 0, initial) { 9 | this.counter = new Int32Array(shared, offset, 1); 10 | if (typeof initial === 'number') { 11 | Atomics.store(this.counter, 0, initial); 12 | } 13 | } 14 | 15 | enter() { 16 | while (true) { 17 | Atomics.wait(this.counter, 0, 0); 18 | const n = Atomics.load(this.counter, 0); 19 | if (n > 0) { 20 | const prev = Atomics.compareExchange(this.counter, 0, n, n - 1); 21 | if (prev === n) return; 22 | } 23 | } 24 | } 25 | 26 | leave() { 27 | Atomics.add(this.counter, 0, 1); 28 | Atomics.notify(this.counter, 0, 1); 29 | } 30 | } 31 | 32 | // Usage 33 | 34 | if (isMainThread) { 35 | const buffer = new SharedArrayBuffer(4); 36 | // Try to change 10 to 2 at next line to check solution 37 | const semaphore = new CountingSemaphore(buffer, 0, 10); 38 | console.dir({ semaphore: semaphore.counter[0] }); 39 | for (let i = 0; i < 20; i++) { 40 | new Worker(__filename, { workerData: buffer }); 41 | } 42 | } else { 43 | const { threadId, workerData } = threads; 44 | const semaphore = new CountingSemaphore(workerData); 45 | const REPEAT_COUNT = 1000000; 46 | const file = `file-${threadId}.dat`; 47 | console.dir({ threadId, semaphore: semaphore.counter[0] }); 48 | 49 | semaphore.enter(); 50 | const data = `Data from ${threadId}`.repeat(REPEAT_COUNT); 51 | fs.writeFile(file, data, () => { 52 | fs.unlink(file, () => { 53 | semaphore.leave(); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /JavaScript/7-binary-safe.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 BinarySemaphore { 10 | constructor(shared, offset = 0, init = false) { 11 | this.lock = new Int32Array(shared, offset, 1); 12 | if (init) Atomics.store(this.lock, 0, UNLOCKED); 13 | } 14 | 15 | enter() { 16 | let prev = Atomics.exchange(this.lock, 0, LOCKED); 17 | while (prev !== UNLOCKED) { 18 | Atomics.wait(this.lock, 0, LOCKED); 19 | prev = Atomics.exchange(this.lock, 0, LOCKED); 20 | } 21 | } 22 | 23 | leave() { 24 | if (Atomics.load(this.lock, 0) === UNLOCKED) { 25 | throw new Error('Cannot leave unlocked BinarySemaphore'); 26 | } 27 | Atomics.store(this.lock, 0, UNLOCKED); 28 | Atomics.notify(this.lock, 0, 1); 29 | } 30 | } 31 | 32 | // Usage 33 | 34 | if (isMainThread) { 35 | const buffer = new SharedArrayBuffer(14); 36 | const semaphore = new BinarySemaphore(buffer, 0, true); 37 | console.dir({ semaphore }); 38 | new Worker(__filename, { workerData: buffer }); 39 | new Worker(__filename, { workerData: buffer }); 40 | } else { 41 | const { threadId, workerData } = threads; 42 | const semaphore = new BinarySemaphore(workerData); 43 | const array = new Int8Array(workerData, 4); 44 | const value = threadId === 1 ? 1 : -1; 45 | 46 | setInterval(() => { 47 | semaphore.enter(); 48 | for (let i = 0; i < 10; i++) { 49 | array[i] += value; 50 | } 51 | console.dir([ threadId, semaphore.lock[0], array ]); 52 | semaphore.leave(); 53 | }, 100); // change to 10 to see race condition 54 | } 55 | -------------------------------------------------------------------------------- /JavaScript/8-await-counting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class CountingSemaphore { 4 | constructor(concurrency) { 5 | this.counter = concurrency; 6 | this.queue = []; 7 | } 8 | 9 | enter() { 10 | return new Promise((resolve) => { 11 | if (this.counter > 0) { 12 | this.counter--; 13 | resolve(); 14 | return; 15 | } 16 | this.queue.push(resolve); 17 | }); 18 | } 19 | 20 | leave() { 21 | if (this.queue.length === 0) { 22 | this.counter++; 23 | return; 24 | } 25 | const resolve = this.queue.shift(); 26 | resolve(); 27 | } 28 | } 29 | 30 | // Usage 31 | 32 | const semaphore = new CountingSemaphore(3); 33 | 34 | const job = async (task) => { 35 | console.log('try enter', task); 36 | await semaphore.enter(); 37 | console.log('enter', task); 38 | setTimeout(() => { 39 | semaphore.leave(); 40 | console.log('leave', task); 41 | }, 1000); 42 | }; 43 | 44 | for (let i = 0; i < 100; i++) job(i); 45 | -------------------------------------------------------------------------------- /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 | ## Binary Semaphore, Counting Semaphore 2 | 3 | [![Семафоры и мьютексы в JavaScript и Node.js](https://img.youtube.com/vi/JNLrITevhRI/0.jpg)](https://www.youtube.com/watch?v=JNLrITevhRI) 4 | --------------------------------------------------------------------------------