Critical Section과 Race Condition

정호윤·2024년 2월 22일
0

동기화

목록 보기
1/4

프로세스와 쓰레드

프로세스(Process)는 실행 중인 프로그램의 추상화이며, 한 개 이상의 쓰레드(Thread)로 이루어집니다. 쓰레드는 프로세스의 실행 단위입니다. 프로세스와 쓰레드는 거의 비슷하지만 몇 가지 중요한 차이가 있습니다.

프로세스는 PCB(Process Control Block)에 프로세스의 정보를 저장하는 반면 쓰레드는 TCB(Thread Control Block)에 저장합니다. 또한 쓰레드의 context switch는 사용 중인 가상 메모리 공간을 바꾸지 않으므로, 프로세스의 context switch에 비해 오버헤드가 적습니다.

하나의 프로세스에서는 여러 쓰레드가 동시에(concurrent) 실행될 수 있고, 이를 멀티쓰레드 프로세스라 부릅니다. 만약, 여러 프로세스가 동시에 공유 자원에 접근하면 동기화 문제가 발생할 수 있습니다.

공유 자원에 접근하는 코드 영역을 critical section이라 합니다. 만약 공유 자원을 여러 프로세스에서 수정하면 문제가 발생하는데, 이를 race condition이라 합니다. Race condition이 발생하면, 공유 자원의 일관성이 깨지는 등의 문제가 발생할 수 있습니다.


Race Condition

두 개의 쓰레드를 생성해 race condition을 발생시켜보겠습니다.

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class RaceConditionExample {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // 스레드 1
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                counter.increment();
            }
        });

        // 쓰레드 2
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                counter.increment();
            }
        });

        // 두 스레드 시작
        thread1.start();
        thread2.start();

        // 메인 스레드가 두 스레드의 실행 완료를 기다림
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.getCount());
    }
}

위는 각 쓰레드에서 count++를 10만 번씩 반복하는 코드입니다. 우리가 기대했던 출력값은 20만이지만, 매번 실행할 때마다 어림없는 값이 출력됩니다.

여기서 공유 자원은 count 변수이며, critical section은 count++입니다. 참고로 count를 여러 쓰레드에서 읽는 것만으로는 문제가 발생하지 않습니다.

그렇다면 이런 race condition은 왜 발생하는 것일까요? 이를 이해하기 위해서는 critical section을 어셈블리어로 변환할 필요가 있습니다. count++를 어셈블리어로 변환하면 컴퓨터가 실제로 어떤 과정을 거쳐 해당 코드를 실행하는지 알 수 있기 때문이죠. count++는 사실 세 가지 명령어로 이루어져 있습니다. count 변수는 메모리의 0x8049a1c 번지에 있고, 그 값은 50이라 가정합니다.

mov 0x8049a1c, %eax
add $0x1, %eax
mov %eax, 0x8049a1c

쓰레드 A와 B가 이 코드를 동시에 실행하면, 다음과 같은 상황이 발생할 수 있습니다. 각 쓰레드는 자신만의 가상 레지스터를 가짐에 유의하세요.

단계스레드 A스레드 B설명
1mov 0x8049a1c, %eax메모리에서 count 값 50을 eax 레지스터로 복사 (eax = 50)
2add $0x1, %eaxeax 레지스터에 1을 더함 (eax = 51)
3context switch
4mov 0x8049a1c, %eax메모리에서 count 값 50을 eax 레지스터로 복사 (eax = 50)
5add $0x1, %eaxeax 레지스터에 1을 더함 (eax = 51)
6mov %eax, 0x8049a1ceax 레지스터의 값 51을 count에 저장
7context switch
8mov %eax, 0x8049a1ceax 레지스터의 값 51을 count에 저장

우리는 위 코드가 올바르게 실행되기를 원합니다. 그러기 위해서는 하나의 쓰레드가 critical section을 실행할 때, 다른 쓰레드는 접근할 수 없도록 해야 합니다. 이를 상호 배제(mutual exclusion)라고 합니다.

상호 배제가 일어나면 위 세 줄의 코드는 atomicity를 보장받습니다. Atomicity란 critical section이 방해받지 않고 모두 실행된다는 의미입니다.

Note
여러 액션을 하나의 atomic한 액션으로 묶은 것을 트랜잭션(transaction)이라 합니다.


출처

  • Operating Systems: Three Easy Pieces

0개의 댓글