본문 바로가기

ComputerScience/OS

[OS] 동기화(Synchronization)와 경쟁 조건(Race Condition), 임계 영역(Critical Section)

하나의 객체를 두 개의 스레드가 접근할 때 발생하는 일

class SharedResource {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

class ThreadA extends Thread {
    private SharedResource resource;

    public ThreadA(SharedResource resource) {
        this.resource = resource;
    }

    public void run() {
        for (int i = 0; i < 1000; i++) {
            resource.increment();
        }
    }
}

class ThreadB extends Thread {
    private SharedResource resource;

    public ThreadB(SharedResource resource) {
        this.resource = resource;
    }

    public void run() {
        for (int i = 0; i < 1000; i++) {
            resource.increment();
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SharedResource resource = new SharedResource();

        ThreadA threadA = new ThreadA(resource);
        ThreadB threadB = new ThreadB(resource);

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();

        System.out.println("Count: " + resource.getCount());
    }
}

위 코드에서 ThreadA와 ThreadB는 동일한 SharedResource 객체에 접근하여 count 값을 증가시킨다.

이 경우, 두 스레드가 동시에 increment() 메소드를 호출하면 race condition이 발생하여 count 값이 예상한 것보다 작게 나올 수 있다. 그 이유는 아래와 같다.

 

increment()를 실행할 때 CPU 레벨에서는 실제로 아래와 같은 코드가 실행된다.

LOAD count to R1
R1 = R1 + 1
STORE R1 to count

R1은 레지스터이며, count는 위 SharedResource 객체의 멤버 변수이다.

위 코드가 실행되면 메모리에 있는 count 값을 +1  더하고 count 값에 저장한다.

 

ThreadA가 R1 = R1 + 1을 수행하던 중에

ThreadB로 컨텍스트 스위칭이 발생하면, ThreadB는 count 값 0을 가져오게 된다. 이후 Thread B가 count 값을 1 더해서 저장하면, Thread A로 다시 컨텍스트 스위칭이 되었을 때 count 값 1로 덮어쓸 수 있다.

따라서 언제 컨텍스트 스위칭이 발생하느냐에 따라 결괏값이 달라지는 현상이 발생한다.

이러한 현상을 Race Condition(경쟁 조건)이라고 한다. 

경쟁 조건(Race Condition)

여러 프로세스/스레드가 동시에 같은 데이터를 조작할 때 타이밍이나 접근 순서에 따라 결과가 달라질 수 있는 상황 

 

이 문제를 해결하기 위해서는 동기화(synchronization) 기법을 사용해야 한다.

동기화(Synchronization)

여러 프로세스/스레드를 동시에 실행해도 공유 데이터의 일관성을 유지하는 것

어떻게 동기화를 할 것 인가?

임계 영역(Critical Section)

공유 데이터의 일관성을 보장하기 위해 하나의 프로세스/스레드만 진입해서 실행 가능한 영역

 

임계 영역이 되기 위한 조건

  1. mutual exclusion (상호 배제): 하나의 영역에 하나의 스레드만 실행해야만 한다.
  2. progress (진행) : 어떤 프로세스나 스레드가 Critical Section에서 진행될 수 있어야 한다.
  3. bounded waiting (한정된 대기) : 무한정 Critical Section에 들어가지 못하고 기다리고 있으면 안 된다.

자바에서는 synchronized 키워드를 사용하여 임계 영역을 만들 수 있다.

class SharedResource {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

increment()를 synchronized 키워드를 통해 한 번에 하나의 스레드만 실행하도록 하여 동기화를 할 수 있다. 

스레드가 synchronized 메서드에 진입하면, lock을 획득하게 된다. 이 때 이 lock은 객체에 대한 lock이다.

그리고 임계영역은 increment() 메서드 내부가 된다.

static synchronized 

synchronized를 사용하지 않을 경우, 각 객체는 자신만의 잠금(lock)을 가지게 된다.

이는 각 객체의 메서드 호출이 독립적으로 동기화되는 것을 의미한다. 이는 스레드가 특정 객체의 synchronized 메서드를 호출하면, 다른 스레드는 동일한 객체의 다른 synchronized 메서드를 호출할 수 없지만, 다른 객체의 synchronized 메서드는 호출할 수 있다.

 

따라서, 여러 스레드가 여러 객체에 대해 동시에 synchronized 메서드를 호출할 수 있으므로, 이 경우에는 race condition이 발생할 수 있다.

 

반면에, static synchronized 메서드는 클래스 레벨에서 동기화되므로, 모든 객체가 동일한 잠금을 공유한다. 따라서 한 스레드가 static synchronized 메서드를 호출하면, 다른 스레드는 동일한 클래스의 다른 static synchronized 메서드를 호출할 수 없다. 이렇게 하면 여러 스레드가 동시에 static synchronized 메서드를 호출하더라도 race condition을 방지할 수 있다. 

class SharedResource {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

위 코드에서 increment()는 statc synchronized 메서드이므로 객체가 아닌 클래스 수준에서의 lock을 획득한다. 

Thread-unsafe를 확인하자.

Java의 SimpleDataFormat 클래스의 문서에는 아래와 같은 내용이 있다.

Synchronization
Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

여러 스레드에서 독립된 인스턴스를 만들어야 하며, 만약 여러 스레드에서 이 객체를 공유한다면, 반드시 동기화가 진행되어야 한다고 적혀있다.

즉 Thread-unsafe 하므로 사용하고자 하는 클래스가 Thread-safe한지 확인할 필요가 있다.

참고