Thread-Safety in Javascript

스레드세이프에 대해서 이야기해보고자 한다

과제를 하는 A와 B 두사람으로 이루어진 한 팀이 있다고 해보자.
이들은 하나의 클라우드저장소 계정하나를 함께 사용하며 이 저장소에 task.ppt 라는 문서를 서로 함께 수정하고있다.
각자 수정할 내용이 있으면 클라우드저장소로부터 파일을 다운로드 받아서 내 컴퓨터에서 수정 후 수정된 파일을 다시 클라우드저장소에 기존의 파일을 덮어쓰는 방법으로 작업하고 있다.

현재 ppt파일에는 hello가 기록되어있다
A가 수정할 내용이 있어서 파일을 다운받아서 hello 뒤에 world 를 붙여서 hello world 를 만드는 작업을 하고있다
A가 작업하는 동안 B도 수정할 내용이 있어서 파일을 다운받았다.
B가 다운 받은 파일에는 hello가 쓰여있다.
그래서 B는 hello 를 지우고 bye 라고 수정하고 수정된 파일을 클라우드저장소에 올렸다.
B가 올리고난 직후인 지금 이 시점에서 클라우드 저장소의 ppt파일에는 bye라고 쓰여있다.
B의 업로드작업이 끝난 이후에 A의 hello world로의 수정작업이 끝나서 A도 작업한 파일을 클라우드 저장소에 올렸다.

최종적으로 클라우드 저장소의 ppt 파일의 내용은 hello world이다
B가 bye라고 수정했던 데이터는 사라진것이다

이런 상황을 스레드세이프하지 않다고 말한다

동시간대에 서로 각각의 작업을 하고있는 A, B를 스레드라고 부를 수 있다.

이 상황이 스레드세이프하기 위해서는 어떻게 해야할까?

그러기 위해서는 A가 수정을 하기 위해서 다운로드를 받아간 시점부터 A가 수정후 클라우드저장소에 올리기 전까지는 그 누구도 파일에 접근할 수 없어야한다
B가 다운로드를 받기 위해서는 A가 모든 일을 마친 이후 시점이여야한다
그러면 위의 문제가 발생하지 않는다

자바스크립트는 싱글스레드로 작동한다고 흔히들 알고있다
그러나 비동기적 작동이 개입되면 스레드세이프하지 않은 상황이 충분히 생길 수 있다
엄밀히 말하자면 자바스크립트에서의 비동기적 특성에 따라 만들어지는 병렬작업을 스레드라는 용어로 칭하지는 않는다
그러나 지금 말하는 이러한 문제점을 말하는 맥락에서 볼때는 스레드의 이러한 특성이 동일하게 적용된다

bGV0IHZhbHVlPTA7CmZ1bmN0aW9uIG1vZGlmeSgpewogICAgbGV0IHdvcmsgPSB2YWx1ZTsgLy8g642w7J207YSwIOuLpOyatOuhnOuTnAogICAgd29yayA9IHdvcmsgKyAxOyAvLyDrjbDsnbTthLAg7IiY7KCVCiAgICB2YWx1ZSA9IHdvcms7IC8vIOuNsOydtO2EsCDsl4XroZzrk5wKfQ==
let value=0; function modify(){ let work = value; // 데이터 다운로드 work = work + 1; // 데이터 수정 value = work; // 데이터 업로드 }
기본적으로 싱글스레드라는 특징을 가지고있다는 점에서 이와 같은 코드는 위에서 말한 스레드세이프하지 않은 상황이 발생하지 않는다
싱글스레드이기 때문에 비동기적 요소가 포함되지 않은 modify 함수의 코드가 작동되는 동안에는 그 어떤 다른 작동이 이루어질 수 없기 때문이다
bGV0IHZhbHVlPTA7CmFzeW5jIGZ1bmN0aW9uIG1vZGlmeSgpewogICAgbGV0IHdvcmsgPSB2YWx1ZTsgLy8g642w7J207YSwIOuLpOyatOuhnOuTnAogICAgd29yayA9IHdvcmsgKyAxOyAvLyDrjbDsnbTthLAg7IiY7KCVCiAgICBhd2FpdCBzb21ldGFzaygpOwogICAgdmFsdWUgPSB3b3JrOyAvLyDrjbDsnbTthLAg7JeF66Gc65OcCn0=
let value=0; async function modify(){ let work = value; // 데이터 다운로드 work = work + 1; // 데이터 수정 await sometask(); value = work; // 데이터 업로드 }
그러나 이와 같이 처리 중에 비동기적 요소가 개입되게되면 위의 스레드세이프하지 못한 상황이 만들어질 수 있다.
그래서 이런 비동기적 요소가 개입이 된다면 개발자가 이 처리를 직렬화해서 순차대로 처리되게 할 필요가 있다.
아래는 스레드세이프함과 그렇지 못한 작동을 코드로 구현해보았다
세이프한 작동은 비동기적 요소가 포함된 작업을 직렬화 해서 구현한 코드이다
버튼을 누를때 마다 value++ 를 해준다
즉 1씩 더해준다는 말이다
스레드세이프하다면 버튼을 10회 눌렀으면 10이 될것이다.
그러나 그렇지 못하면 10보다 부족한 값이 된다.
Y2xhc3MgVGhyZWFkU2FmZVZhbHVlIHsKICAgIHZhbHVlOwogICAgI2xvY2s7CiAgICAjcXVldWUgPSBbXTsKICAgIGNvbnN0cnVjdG9yKHYpIHsKICAgICAgIHRoaXMudmFsdWUgPSB2OwogICAgfQogICAgYXN5bmMgcnVuVGFzayhjYikgewogICAgICAgbGV0IGFyZyA9IFsuLi5hcmd1bWVudHNdLnNwbGljZSgxKTsKICAgICAgIGNvbnN0IHF1ZXVlID0gdGhpcy4jcXVldWU7CiAgICAgICBjb25zdCB0YXNrID0gYXN5bmMgZnVuY3Rpb24gKCkgewogICAgICAgICAgY29uc3QgeyByZXNvbHZlLCBfdGhpcyB9ID0gdGhpczsKICAgICAgICAgIF90aGlzLiNsb2NrID0gdHJ1ZTsKICAgICAgICAgIGF3YWl0IGNiLmJpbmQoX3RoaXMpKC4uLmFyZyk7CiAgICAgICAgICBfdGhpcy4jbG9jayA9IGZhbHNlOwogICAgICAgICAgcXVldWUubGVuZ3RoICYmIHF1ZXVlLnNoaWZ0KCkoKTsKICAgICAgICAgIHJlc29sdmUoKTsKICAgICAgIH07CiAgICAgICByZXR1cm4gYXdhaXQgbmV3IFByb21pc2UocmVzb2x2ZSA9PiB7CiAgICAgICAgICBxdWV1ZS5wdXNoKHRhc2suYmluZCh7IHJlc29sdmUsIF90aGlzOiB0aGlzIH0pKTsKICAgICAgICAgIGlmICghdGhpcy4jbG9jaykgcXVldWUuc2hpZnQoKSgpOwogICAgICAgfSkKICAgIH07CiB9CiBsZXQgZGVsYXkgPSBhc3luYyBmdW5jdGlvbiAodCkgewogICAgYXdhaXQgbmV3IFByb21pc2UociA9PiBzZXRUaW1lb3V0KHIsIHQpKTsKIH0KIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdsb2FkJywgZnVuY3Rpb24gKCkgewogICAgewogICAgICAgbGV0IGNvbnRhaW5lciA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpOwogICAgICAgY29udGFpbmVyLmlubmVySFRNTCA9IGAKICAgICAgIFBMVVPtgbTrpq3tlaDrlYzrp4jri6QgdmFsdWXrs4DsiJjsl5AgMeydhCDrjZTtlanri4jri6Q8YnIgLz4KICAgICAgIDxociAvPgogICAgICAg7Iqk66CI65Oc7IS47J207ZSE7ZWcIOuwqeuylTxiciAvPgogICAgICAgPGRpdiBpZD0ic2FmZXN0YXRlIj48L2Rpdj4KICAgICAgIDxidXR0b24gaWQ9InNhZmV0MSI+VGhyZWFkIFNhZmUgUExVUzwvYnV0dG9uPgogICAgICAgPGRpdiBpZD0ic2FmZWxpc3QiPjwvZGl2PgogICAgICAgPGhyIC8+CiAgICAgICDsiqTroIjrk5zshLjsnbTtlITtlZjsp4Ag7JWK7J2AIOuwqeuylTxiciAvPgogICAgICAgPGRpdiBpZD0idW5zYWZlc3RhdGUiPjwvZGl2PgogICAgICAgPGJ1dHRvbiBpZD0idW5zYWZldDEiPlRocmVhZCBVbnNhZmUgUExVUzwvYnV0dG9uPgogICAgICAgPGRpdiBpZD0idW5zYWZlbGlzdCI+PC9kaXY+CiAgICAgICBgOwogICAgICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2NvbnRhaW5lcicpLmFwcGVuZENoaWxkKGNvbnRhaW5lcik7CiAgICB9CiAgICB7CiAgICAgICBsZXQgaW5jcmVtZW50ID0gMDsKICAgICAgIGxldCB2YWx1ZSA9IG5ldyBUaHJlYWRTYWZlVmFsdWUoMCk7CiAgICAgICBsZXQgdGFzayA9IGFzeW5jIGZ1bmN0aW9uIChwaWQsIGljb24pIHsKICAgICAgICAgIGljb24uY2xhc3NOYW1lID0gJ3Byb2Nlc3NpbmcnOwogICAgICAgICAgaWNvbi5zZXRUZXh0KGBJRCgke3BpZH0pIOyekeyXhSAtIOyLnOyekWApOwogICAgICAgICAgYXdhaXQgZGVsYXkoNTAwICogTWF0aC5yYW5kb20oKSk7CiAgICAgICAgICBsZXQgdiA9IHRoaXMudmFsdWU7CiAgICAgICAgICBpY29uLnNldFRleHQoYElEKCR7cGlkfSkg7J6R7JeFIC0g6rCS7J296riwICR7dGhpcy52YWx1ZX1gKTsKICAgICAgICAgIGF3YWl0IGRlbGF5KDUwMCAqIE1hdGgucmFuZG9tKCkpOwogICAgICAgICAgdiA9IHYgKyAxOwogICAgICAgICAgdGhpcy52YWx1ZSA9IHY7CiAgICAgICAgICBpY29uLnNldFRleHQoYElEKCR7cGlkfSkg7J6R7JeFIC0g6rCS7JOw6riwICR7dGhpcy52YWx1ZX1gKTsKICAgICAgICAgIGF3YWl0IGRlbGF5KDUwMCAqIE1hdGgucmFuZG9tKCkpOwogICAgICAgICAgaWNvbi5zZXRUZXh0KGBJRCgke3BpZH0pIOyekeyXhSAtIOyiheujjGApOwogICAgICAgICAgYXdhaXQgZGVsYXkoNTAwICogTWF0aC5yYW5kb20oKSk7CiAgICAgICB9OwogICAgICAgc2FmZXQxLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgYXN5bmMgKCkgPT4gewogICAgICAgICAgbGV0IHBpZCA9ICsraW5jcmVtZW50OwogICAgICAgICAgcmVuZGVyU3RhdGUoKTsKICAgICAgICAgIGxldCBpY29uID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7CiAgICAgICAgICBpY29uLnNldFRleHQgPSBmdW5jdGlvbiAodGV4dCkgewogICAgICAgICAgICAgdGhpcy5pbm5lclRleHQgPSB0ZXh0OwogICAgICAgICAgICAgY29uc29sZS5sb2codGV4dCk7CiAgICAgICAgICB9CiAgICAgICAgICBpY29uLmNsYXNzTmFtZSA9ICdwZW5kaW5nJzsKICAgICAgICAgIHNhZmVsaXN0LmFwcGVuZENoaWxkKGljb24pOwogICAgICAgICAgaWNvbi5pbm5lclRleHQgPSBgSUQoJHtwaWR9KSDsnpHsl4Ug64yA6riw7KSRYDsKICAgICAgICAgIGF3YWl0IHZhbHVlLnJ1blRhc2soYXN5bmMgZnVuY3Rpb24gKCkgewogICAgICAgICAgICAgYXdhaXQgdGFzay5iaW5kKHRoaXMpKC4uLmFyZ3VtZW50cyk7CiAgICAgICAgICB9LCBwaWQsIGljb24pOwogICAgICAgICAgaWNvbi5yZW1vdmUoKTsKICAgICAgICAgIHJlbmRlclN0YXRlKCk7CiAgICAgICB9KTsKICAgICAgIGZ1bmN0aW9uIHJlbmRlclN0YXRlKCkgewogICAgICAgICAgc2FmZXN0YXRlLmlubmVyVGV4dCA9IChg7LSdIO2BtOumreyImDoke2luY3JlbWVudH0sIOqwkjoke3ZhbHVlLnZhbHVlfWApOwogICAgICAgfQogICAgfQogICAgewogICAgICAgbGV0IGluY3JlbWVudCA9IDA7CiAgICAgICBsZXQgdmFsdWUgPSAwOwogICAgICAgbGV0IHRhc2sgPSBhc3luYyBmdW5jdGlvbiAocGlkLCBpY29uKSB7CiAgICAgICAgICBpY29uLmNsYXNzTmFtZSA9ICdwcm9jZXNzaW5nJzsKICAgICAgICAgIGljb24uc2V0VGV4dChgSUQoJHtwaWR9KSDsnpHsl4UgLSDsi5zsnpFgKTsKICAgICAgICAgIGF3YWl0IGRlbGF5KDUwMCAqIE1hdGgucmFuZG9tKCkpOwogICAgICAgICAgbGV0IHYgPSB2YWx1ZTsKICAgICAgICAgIGljb24uc2V0VGV4dChgSUQoJHtwaWR9KSDsnpHsl4UgLSDqsJLsnb3quLAgJHt2YWx1ZX1gKTsKICAgICAgICAgIGF3YWl0IGRlbGF5KDUwMCAqIE1hdGgucmFuZG9tKCkpOwogICAgICAgICAgdiA9IHYgKyAxOwogICAgICAgICAgdmFsdWUgPSB2OwogICAgICAgICAgaWNvbi5zZXRUZXh0KGBJRCgke3BpZH0pIOyekeyXhSAtIOqwkuyTsOq4sCAke3ZhbHVlfWApOwogICAgICAgICAgYXdhaXQgZGVsYXkoNTAwICogTWF0aC5yYW5kb20oKSk7CiAgICAgICAgICBpY29uLnNldFRleHQoYElEKCR7cGlkfSkg7J6R7JeFIC0g7KKF66OMYCk7CiAgICAgICAgICBhd2FpdCBkZWxheSg1MDAgKiBNYXRoLnJhbmRvbSgpKTsKICAgICAgIH07CiAgICAgICB1bnNhZmV0MS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGFzeW5jICgpID0+IHsKICAgICAgICAgIGxldCBwaWQgPSArK2luY3JlbWVudDsKICAgICAgICAgIHJlbmRlclN0YXRlKCk7CiAgICAgICAgICBsZXQgaWNvbiA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpOwogICAgICAgICAgaWNvbi5zZXRUZXh0ID0gZnVuY3Rpb24gKHRleHQpIHsKICAgICAgICAgICAgIHRoaXMuaW5uZXJUZXh0ID0gdGV4dDsKICAgICAgICAgICAgIGNvbnNvbGUubG9nKHRleHQpOwogICAgICAgICAgfQogICAgICAgICAgaWNvbi5jbGFzc05hbWUgPSAncGVuZGluZyc7CiAgICAgICAgICB1bnNhZmVsaXN0LmFwcGVuZENoaWxkKGljb24pOwogICAgICAgICAgaWNvbi5pbm5lclRleHQgPSBgSUQoJHtwaWR9KSDsnpHsl4Ug64yA6riw7KSRYDsKICAgICAgICAgIGF3YWl0IHRhc2socGlkLCBpY29uKTsKICAgICAgICAgIGljb24ucmVtb3ZlKCk7CiAgICAgICAgICByZW5kZXJTdGF0ZSgpOwogICAgICAgfSk7CiAgICAgICBmdW5jdGlvbiByZW5kZXJTdGF0ZSgpIHsKICAgICAgICAgIHVuc2FmZXN0YXRlLmlubmVyVGV4dCA9IChg7LSdIO2BtOumreyImDoke2luY3JlbWVudH0sIOqwkjoke3ZhbHVlfWApOwogICAgICAgfQogICAgfQogfSk7
class ThreadSafeValue { value; #lock; #queue = []; constructor(v) { this.value = v; } async runTask(cb) { let arg = [...arguments].splice(1); const queue = this.#queue; const task = async function () { const { resolve, _this } = this; _this.#lock = true; await cb.bind(_this)(...arg); _this.#lock = false; queue.length && queue.shift()(); resolve(); }; return await new Promise(resolve => { queue.push(task.bind({ resolve, _this: this })); if (!this.#lock) queue.shift()(); }) }; } let delay = async function (t) { await new Promise(r => setTimeout(r, t)); } window.addEventListener('load', function () { { let container = document.createElement('div'); container.innerHTML = ` PLUS클릭할때마다 value변수에 1을 더합니다<br /> <hr /> 스레드세이프한 방법<br /> <div id="safestate"></div> <button id="safet1">Thread Safe PLUS</button> <div id="safelist"></div> <hr /> 스레드세이프하지 않은 방법<br /> <div id="unsafestate"></div> <button id="unsafet1">Thread Unsafe PLUS</button> <div id="unsafelist"></div> `; document.getElementById('container').appendChild(container); } { let increment = 0; let value = new ThreadSafeValue(0); let task = async function (pid, icon) { icon.className = 'processing'; icon.setText(`ID(${pid}) 작업 - 시작`); await delay(500 * Math.random()); let v = this.value; icon.setText(`ID(${pid}) 작업 - 값읽기 ${this.value}`); await delay(500 * Math.random()); v = v + 1; this.value = v; icon.setText(`ID(${pid}) 작업 - 값쓰기 ${this.value}`); await delay(500 * Math.random()); icon.setText(`ID(${pid}) 작업 - 종료`); await delay(500 * Math.random()); }; safet1.addEventListener('click', async () => { let pid = ++increment; renderState(); let icon = document.createElement('div'); icon.setText = function (text) { this.innerText = text; console.log(text); } icon.className = 'pending'; safelist.appendChild(icon); icon.innerText = `ID(${pid}) 작업 대기중`; await value.runTask(async function () { await task.bind(this)(...arguments); }, pid, icon); icon.remove(); renderState(); }); function renderState() { safestate.innerText = (`총 클릭수:${increment}, 값:${value.value}`); } } { let increment = 0; let value = 0; let task = async function (pid, icon) { icon.className = 'processing'; icon.setText(`ID(${pid}) 작업 - 시작`); await delay(500 * Math.random()); let v = value; icon.setText(`ID(${pid}) 작업 - 값읽기 ${value}`); await delay(500 * Math.random()); v = v + 1; value = v; icon.setText(`ID(${pid}) 작업 - 값쓰기 ${value}`); await delay(500 * Math.random()); icon.setText(`ID(${pid}) 작업 - 종료`); await delay(500 * Math.random()); }; unsafet1.addEventListener('click', async () => { let pid = ++increment; renderState(); let icon = document.createElement('div'); icon.setText = function (text) { this.innerText = text; console.log(text); } icon.className = 'pending'; unsafelist.appendChild(icon); icon.innerText = `ID(${pid}) 작업 대기중`; await task(pid, icon); icon.remove(); renderState(); }); function renderState() { unsafestate.innerText = (`총 클릭수:${increment}, 값:${value}`); } } });