[Operating Systems] 멀티스레드와 레이스 컨디션

실제로 여러 스레드가 같은 데이터를 동시에 건드릴 때 어떤 문제가 생기는지, 그리고 이를 어떻게 해결하는지 알아보고자 한다.


🧵 멀티스레드 기본 개념

🎯 멀티스레드란?

멀티스레드는 하나의 프로그램에서 여러 작업을 동시에 처리하는 기술이다. 실제로는 CPU가 매우 빠르게 스레드들을 번갈아가며 실행시켜서 동시에 실행되는 것처럼 보이게 한다.

예를 들어, 음악을 들으면서 웹서핑을 하고 파일을 다운로드하는 것도 모두 멀티스레드 덕분이다.

// Web Worker를 이용한 멀티스레드
const worker1 = new Worker("worker1.js");
const worker2 = new Worker("worker2.js");

// 두 워커가 동시에 서로 다른 작업 수행
worker1.postMessage("계산 작업 시작");
worker2.postMessage("파일 처리 시작");

🔄 스레드 생성과 실행

스레드를 만들고 실행하는 과정은 의외로 간단하다. 하지만 여러 스레드가 함께 동작할 때 문제가 시작된다.

스레드의 기본 동작:

  1. 생성: 새로운 스레드 객체 만들기
  2. 시작: 스레드가 실제로 실행되도록 하기
  3. 실행: 할당된 작업 수행
  4. 종료: 작업 완료 후 리소스 정리
// 워커 스레드 예제
let count = 0;

self.onmessage = function (e) {
  const { action, iterations } = e.data;

  if (action === "count") {
    for (let i = 0; i < iterations; i++) {
      count++;
    }
    self.postMessage(`카운트 완료: ${count}`);
  }
};

🤝 공유 자원의 문제

멀티스레드의 가장 큰 장점이면서 동시에 가장 큰 문제점이 바로 메모리 공유다.

여러 스레드가 같은 변수나 객체에 접근할 수 있어서 데이터를 쉽게 주고받을 수 있지만, 동시에 접근할 때 예상치 못한 결과가 나올 수 있다.

마치 여러 사람이 동시에 같은 은행 계좌에서 돈을 뽑으려고 할 때 잔고 계산이 엉망이 되는 것과 비슷하다.


⚡ 레이스 컨디션 - 스레드들의 경주

🏁 레이스 컨디션이란?

레이스 컨디션은 두 개 이상의 스레드가 공유 자원에 동시에 접근할 때, 실행 순서에 따라 결과가 달라지는 현상이다.

예를 들어, 두 사람이 동시에 같은 문서를 수정한다고 생각해보자.

둘 다 내용을 바꾸고 각각 저장을 누르면, 누가 마지막에 저장했느냐에 따라 문서의 최종 내용이 달라질 수 있다. 이는 마치 경쟁하듯이 자원을 다루는 상황이고, 바로 이런 경합 상태가 레이스 컨디션이다!

프로그래밍에서는 종종 하나의 변수를 여러 스레드가 동시에 수정할 때 이런 문제가 발생한다.

어떤 공유된 숫자 값을 1000번 증가시키는 작업을 두 스레드가 각각 동시에 실행한다고 가정해 보자.

이론적으로는 최종 값이 2000이 되어야 하지만, 실제 실행해보면 1000에서 2000 사이의 예측 불가능한 값이 나오게 된다.

💥 왜 문제가 생길까?

증가 연산이 단순히 숫자를 1 더하는 것이 아니라, 실제로는 세 단계로 나뉜다.

  1. 읽기: 현재 값을 메모리에서 가져오기
  2. 계산: 값에 1을 더하기
  3. 쓰기: 결과를 다시 메모리에 저장하기

두 스레드가 동시에 이 과정을 수행하면:

시간    스레드1        스레드2        sharedCounter
 1     읽기(0)                       0
 2                   읽기(0)         0
 3     계산(0+1)                     0
 4                   계산(0+1)       0
 5     쓰기(1)                       1
 6                   쓰기(1)         1  ← 1이 두 번 증가했는데 결과는 1!

위와 같이 여러 스레드가 동시에 이 과정을 수행하게 되면 중간 단계에서 엉키게 되고, 결과적으로 원하는 만큼 증가하지 않게 되는 것이다.


🛡️ 동기화

🔒 동기화란?

동기화는 여러 스레드가 동시에 공유 자원에 접근할 때, 접근 순서를 제어하여 데이터 충돌이나 예기치 않은 동작을 방지하는 메커니즘이다.

마치 여러 사람이 은행에 와서 ATM을 이용하려고 할 때, 한 명씩 줄을 서서 순서대로 사용하는 것과 같다.

모두가 동시에 버튼을 누르면 계좌 금액이 잘못 처리될 수 있으므로, 질서 있는 접근이 필수적이다.

🚪 뮤텍스

뮤텍스는 “Mutual Exclusion(상호 배제)”의 줄임말로, 한 번에 하나의 스레드만 특정 코드 영역을 실행할 수 있게 하는 동기화 도구다.

동작 원리:

  1. 스레드가 공유 자원에 접근하려면 먼저 뮤텍스를 획득해야 한다.
  2. 뮤텍스를 가진 스레드만 공유 자원에 접근할 수 있으며, 나머지는 대기한다.
  3. 작업 완료 후 뮤텍스를 해제한다.
  4. 다음 순서의 스레드가 뮤텍스를 획득해 접근한다.

마치 ATM 기기 앞에 하나뿐인 순번표를 뽑고 기다리는 것처럼, 뮤텍스를 가진 즉 순번표를 가진 사람만 ATM을 사용할 수 있고, 이용이 끝나면 다음 사람이 사용할 수 있다.

⚛️ 원자적 연산

원자적 연산(Atomic Operation)은 중간에 방해받지 않고 한 번에 완료되는 연산이다.

이는 여러 스레드가 같은 데이터를 동시에 수정하려 할 때 충돌 없이 정확한 결과를 보장하기 위한 핵심 개념이다.

JavaScript에서는 Atomics 객체를 통해 원자적 연산을 수행할 수 있고, 공유 메모리 환경에서 안전한 동시성 처리를 가능하게 해준다.

ATM 사용 도중에는 기기가 멈추지 않고 한 번에 작업을 끝내는 것처럼, 원자적 연산은 도중에 끼어들 수 없는 연산이다.

🔐 임계 영역

임계 영역(Critical Section)은 공유 자원에 접근하는 코드 부분으로, 한 번에 하나의 스레드만 실행되어야 하는 영역이다.

이 영역을 보호하지 않으면 레이스 컨디션이 발생해 예상치 못한 결과를 초래할 수 있다.

ATM 앞 한 명이 서서 거래하는 구간이 바로 임계 영역이다. 여러 사람이 동시에 이용할 수 없다.


Leave a comment