공유 리소스는 프로그래밍에서 중요한 부분이다. 여러 스레드 또는 비동기 작업이 공유 리소스에 동시에 액세스할 수 있으므로 동기화 문제와 데이터 경합이 발생할 수 있다. 이러한 문제를 방지하려면 공유 리소스에 대한 액세스가 동기화되고 안전한지 확인해야 한다. Node.js에서 async-mutex npm 모듈로 공유 리소스에 대한 액세스를 간단하고 효과적으로 동기화할 수 있다. 이 글에서는 async-mutex 모듈을 사용하여 Node.js에서 공유 리소스의 동기화를 처리하는 방법을 알아보자.
async-mutex 모듈은 무엇인가.
async-mutex 모듈을 통해 공유 리소스에 대한 액세스를 동기화를 위한 Mutex 클래스를 사용할 수 있다. 뮤텍스는 공유 리소스가 사용되는 동안 다른 작업이 공유 리소스에 액세스하지 못하도록 하는 데 사용할 수 있는 간단한 잠금 장치이다. Mutex 클래스는 뮤텍스를 획득하는 데 사용할 수 있는 acquire() 메서드와 뮤텍스를 해제하는 데 사용할 수 있는 release() 메서드를 가지고 있다.
const Mutex = require('async-mutex').Mutex;
// Create a new mutex
const mutex = new Mutex();
// Define a shared resource (in this case, a counter)
let counter = 0;
// Define a function that increments the counter
async function incrementCounter() {
// Acquire the mutex to lock the counter
await mutex.acquire();
try {
// Increment the counter
console.log(`Counter is now ${counter}`);
counter++;
} finally {
// Release the mutex to unlock the counter
mutex.release();
}
}
// Call the function multiple times concurrently
incrementCounter();
incrementCounter();
incrementCounter();
incrementCounter는 비동기(async) 함수로, 뮤텍스가 없는 경우 임계 구역(critical section)이 순차적으로 실행된다는 보장을 할 수 없다. 물론 자바스크립트가 단일 스레드로 동작하기 때문에 이 부분에 의구심을 가질 수 있을 수도 있다. 그러나 임계 구역 내에 I/O 자원이나 대기 등의 작업을 할 수 있다는 생각을 해보면 이해에 도움이 될 것이다.
뮤텍스는 한 번에 하나의 작업만 카운터에 액세스할 수 있도록 하여 경합 조건(race condition)과 동기화 문제를 방지한다.
async-mutex 모듈을 사용하는 방법.
Node.js 애플리케이션에서 async-mutex 모듈을 사용하려면 먼저 npm을 사용하여 모듈을 설치해야 한다.
npm install async-mutex
모듈이 설치되면 Mutex 클래스를 사용하여 새 뮤텍스를 만들 수 있다.
const Mutex = require('async-mutex').Mutex;
// Create a new mutex
const mutex = new Mutex();
그런 다음 뮤텍스를 사용하여 공유 리소스에 대한 액세스를 동기화할 수 있다.
const { Mutex } = require('async-mutex');
const mutex = new Mutex();
async function exampleFunction() {
const release = await mutex.acquire();
console.log('Mutex acquired');
// Perform some critical section operation here
release();
}
exampleFunction();
exampleFunction 내에서 먼저 acquire() 메서드를 호출한다. 이 메서드는 뮤텍스가 획득되면 release 함수와 함께 resolve되는 Promise를 반환한다. 이 반환값을 release라는 변수에 저장한다.
이후 release 함수를 호출하여 뮤텍스를 해제한다. 이렇게 하면 프로그램의 다른 부분에서 뮤텍스를 획득하고 자체적으로 임계 영역(critical section) 작업을 수행할 수 있다.
좀 더 실전적인 예제를 보도록 하자.
const { Mutex } = require('async-mutex');
const mutex = new Mutex();
async function exampleFunction(id) {
const release = await mutex.acquire();
console.log(`Mutex acquired by ${id}`);
// Simulate some critical section operation here
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Mutex released by ${id}`);
release();
}
async function main() {
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(exampleFunction(i));
}
await Promise.all(promises);
}
main();
exampleFunction 내에서 뮤텍스를 해제하기 전에 1초의 지연을 추가하여 임계 영역 작업을 시뮬레이션한다. 프로그램의 어느 부분에서 뮤텍스를 획득하고 해제했는지를 나타내는 메시지를 콘솔에 표시한다.
$ node example.js
Mutex acquired by 0
Mutex released by 0
Mutex acquired by 1
Mutex released by 1
Mutex acquired by 2
Mutex released by 2
Mutex acquired by 3
Mutex released by 3
Mutex acquired by 4
Mutex released by 4
임계 영역에 대한 경합이 발생하지 않는다는 것을 확인할 수 있다.
이제 잠금을 해제하는 쪽에 주목해보자. release() 함수는 잠금을 해제하는 데 사용된다. 이 함수는 뮤텍스의 잠금을 해제하여 다른 비동기 함수가 잠금을 획득할 수 있도록 한다.
const mutex = new Mutex();
async function criticalSection() {
await mutex.acquire();
try {
// critical section
console.log('In critical section');
} finally {
mutex.release();
}
}
criticalSection();
try-finally 블록은 임계 영역에서 예외가 발생하더라도 release() 함수가 항상 호출되도록 할 수 있다.
마무리
뮤텍스는 동시성 프로그래밍에서 경쟁 조건을 방지하고 데이터 무결성을 보장하는 데 필수적인 도구이다. async-mutex 패키지는 Node.js의 비동기 코드에 대한 뮤텍스를 쉽게 구현할 수 있도록 한다. Mutex 클래스를 사용하면 한 번에 하나의 비동기 함수만 코드의 중요한 섹션에 액세스할 수 있도록 하여 경쟁 조건과 데이터 손상을 방지할 수 있다.