오늘도 기록하는 중 GitHub

CS/OS

동기화

YongE 2025. 6. 10. 14:40

프로세스 동기화


프로세스 동기화는 현대 멀티프로세싱 시스템에서 가장 중요한 개념 중 하나다. 여러 프로세스가 동시에 실행되는 환경에서 공유 자원에 대한 안전한 접근을 보장하기 위해 반드시 필요한 기술이다. 동기화는 단순히 프로세스 간의 실행 순서를 제어하는 것을 넘어서, 데이터의 일관성과 시스템의 안정성을 보장하는 핵심적인 역할을 수행한다.

동기화란 무엇인가

동기화(Synchronization)는 시스템 프로세스 간 정보를 공유하는 행위로 정의할 수 있다. 보다 구체적으로 말하면, 여러 프로세스나 스레드가 공유 자원에 접근할 때 발생할 수 있는 문제를 예방하고 올바른 실행 순서를 보장하는 메커니즘이다. 현대 컴퓨터는 다중 프로그래밍 시스템으로 여러 프로세스가 동시에 실행되며, 이러한 환경에서 동기화는 필수불가결한 요소가 되었다.

동기화의 핵심은 두 가지다. 첫째는 실행 순서 제어로, 프로세스들이 올바른 순서대로 실행되도록 하는 것이다. 예를 들어, 메모장 프로그램에서 입력 프로세스와 검사 프로세스가 있을 때, 입력이 완료된 후에 검사가 이루어져야 한다. 둘째는 상호 배제(Mutual Exclusion)로, 동시에 접근해서는 안 되는 자원에 하나의 프로세스만 접근하게 하는 것이다.

동기화가 필요한 이유

여러 프로세스가 같은 자원을 공유하고 동시에 접근해야 하는 상황에서 문제가 발생할 수 있다.

가장 대표적인 예가 계좌 잔액 문제다. 한 계좌에 10만원이 있고, 프로세스1이 2만원, 프로세스2가 4만원을 입금한다고 가정해보자. 프로세스1이 계좌 금액에 2만원을 더했는데 저장하기 직전에 문맥 교환이 일어나서 프로세스2가 실행된다면, 프로세스2는 프로세스1이 변경한 값이 아닌 원래 값인 10만원을 읽게 된다. 결국 최종 결과는 16만원이 아닌 14만원이 되어 데이터 일관성이 깨지게 된다.

이를 해결하기 위해 운영체제는 프로세스를 실행 순서와 상호 배제에 기반해 제어할 필요가 있다.

공유 자원과 임계구역

공유 자원

공유 자원(Shared Resource)은 여러 프로세스들이 공동으로 이용하는 변수, 메모리 공간, 파일, 입출력 장치 등을 의미한다. 이러한 공유 자원은 시스템의 효율성을 높이는 중요한 요소이지만, 동시에 동기화 문제의 근본적인 원인이기도 하다. 공유 데이터(Shared Data)나 임계 데이터(Critical Data)라고도 불리는 이러한 자원들은 적절한 관리 없이는 시스템의 안정성을 해칠 수 있다.

임계구역

임계구역(Critical Section)은 공유 자원에 접근하는 코드의 영역을 의미한다. 더 정확히 말하면, 여러 프로세스가 동시에 접근하면 문제가 발생할 수 있는 코드 영역이다. 임계구역은 멀티스레드 시스템에서 스레드가 공통의 변수를 변경하거나, 테이블을 업데이트하거나, 파일에 쓰는 작업을 수행하는 코드 부분으로 정의된다.

임계구역에 대한 접근은 반드시 제어되어야 한다. 두 개 이상의 프로세스가 임계구역에 진입하려 할 때는 하나만 진입이 가능하고 나머지는 대기해야 한다. 진입한 프로세스가 작업을 완료하면 그때서야 대기 중인 다른 프로세스가 진입할 수 있다. 이러한 메커니즘을 통해 데이터의 일관성과 시스템의 안정성을 보장할 수 있다.

레이스 컨디션과 동기화 문제

레이스 컨디션의 정의

레이스 컨디션(Race Condition)은 두 개 이상의 프로세스 또는 스레드가 공유 자원을 서로 사용하려고 경합하는 현상을 의미한다. 이는 자원을 공유할 때 발생하는 경쟁 상태로, 자원의 동기화에서 문제가 발생한다. 레이스 컨디션이 발생하면 프로세스의 실행 순서에 따라 결과가 달라지는 문제가 생긴다.

멀티스레드 환경에서는 프로세스 내의 모든 자원을 공유할 수 있다는 점에서 동기화 문제가 더욱 심각해진다. 예를 들어, 두 개의 스레드가 변수 a에 11이라는 값을 저장하려고 할 때, 스레드 A가 변수 a를 읽고 10을 더한 값을 저장하고, 스레드 B가 변수 a를 읽고 1을 더한 값을 저장한다면, 최종 결과는 11이 아닌 1이 될 수 있다.

동기화 문제의 유형

동기화 문제는 레이스 컨디션 외에도 여러 형태로 나타날 수 있다. 교착 상태(Deadlock)는 공유 자원에 대한 요구가 엉켜서 프로세스나 스레드가 자원의 락을 획득하기 위해 무한 대기하는 상황이다. 기아 상태(Starvation)는 스레드들에게 우선순위를 부여하여 공유 자원에 접근할 때 우선순위가 낮은 스레드가 소외되어 아무 일도 하지 못하는 상태를 의미한다.

이러한 문제들을 해결하기 위해서는 적절한 동기화 메커니즘이 필요하다. 상호 배제 조건을 통해서 동시에 공유 자원에 접근할 수 없도록 하는 것이 기본적인 해결 방법이지만, 이로 인해 다른 문제가 발생할 수도 있어 신중한 설계가 필요하다.

임계구역 문제 해결 조건

상호 배제 원칙

임계구역 문제를 해결하기 위해서는 세 가지 필수 조건을 만족해야 한다. 첫 번째는 상호 배제(Mutual Exclusion) 조건으로, 한 프로세스가 임계구역에 진입했다면 다른 프로세스는 진입할 수 없어야 한다는 원칙이다. 이는 임계구역에서 작업 중인 프로세스가 있으면, 다른 프로세스는 임계구역에 접근하지 못하게 하는 핵심 메커니즘이다.

상호 배제를 구현하기 위해서는 최소한 두 가지 기본 연산(Primitives)이 필요하다. 첫째는 enter CS() primitive로 프로세스가 임계구역에 진입했는지를 나타내는 정보이고, 둘째는 exit CS() primitive로 프로세스가 임계구역을 벗어났다는 것을 알리는 정보다.

진행과 유한 대기 조건

두 번째 조건은 진행(Progress) 조건으로, 임계구역에 진입한 프로세스가 없다면 들어가려고 하는 프로세스는 진입할 수 있어야 한다는 원칙이다. 이는 시스템의 효율성을 보장하기 위한 조건으로, 임계구역이 비어있는데도 프로세스가 진입하지 못하는 상황을 방지한다.

세 번째 조건은 유한 대기(Bounded Waiting) 조건으로, 진입 대기 중인 프로세스가 있다면 언젠가는 진입할 수 있어야 한다는 원칙이다. 즉, 무한정 대기하지 않아야 하며, 대기 시간이 무한정으로 길어져서는 안 된다. 이는 기아 상태를 방지하기 위한 중요한 조건이다.

뮤텍스 락 (Mutex Lock)

뮤텍스 락(Mutex Lock)은 Mutual Exclusion Lock의 줄임말로, 임계구역 문제에 대한 소프트웨어 기반 해결책 중 가장 간단한 도구다. 뮤텍스 락은 프로세스가 임계구역에 들어가면 자물쇠를 잠그고, 임계구역에서 나올 때 자물쇠를 푸는 기법이다. 여기서 자물쇠 역할을 하는 것이 바로 락(Lock)이다.

뮤텍스 락은 available이라는 boolean 변수를 가지며, 이 변수 값이 락의 가용 여부를 표시한다. 락이 사용 가능하면 acquire() 호출이 성공하고 락은 사용 불가 상태가 되며, 사용 불가 상태의 락을 획득하려고 하면 프로세스는 락이 반환될 때까지 봉쇄된다.

뮤텍스 락의 구현

뮤텍스 락은 1개의 전역 변수(lock)와 두 개의 함수로 구현할 수 있다. acquire 함수는 임계구역에 진입하기 전에 호출하는 함수로, 진입 전에 lock이 걸려 있다면 풀릴 때까지 반복적으로 확인한 뒤 풀려 있다면 진입하여 lock을 거는 역할을 한다. release 함수는 임계구역에서 작업이 끝난 뒤에 호출하는 함수로, lock을 풀어서 임계구역을 열어놓는 역할을 한다.

acquire() {
    while(!available); // busy wait
    available = false;
}

release() {
    available = true;
}

acquire()과 release() 함수 호출은 원자적(atomic)으로 수행되어야 한다. 이는 Compare-And-Swap(CAS)와 같은 하드웨어 지원을 통해 구현할 수 있다.

바쁜 대기

공유자원을 사용할 수 있을 때까지 프로세스는 계속 락이 풀렸는지 확인하며 대기해야 한다. 이러한 대기 방식을 바쁜 대기(Busy Wait)라고 한다. 바쁜 대기는 프로세스가 임계구역에 있는 동안 진입을 원하는 다른 프로세스들이 acquire() 함수를 호출하는 반복문을 계속 실행해야 한다는 단점이 있다.

세마포어 (Semaphore)

세마포어(Semaphore)는 다익스트라가 고안한 동기화 도구로, 두 개의 원자적 함수로 조작되는 정수 변수다. 세마포어는 뮤텍스와 유사하게 동작하지만 프로세스의 행동을 더 정교하게 동기화할 수 있는 방법을 제공한다. 임계구역이 여러 개 있다는 가정 하에 만들어진, 더 일반화된 동기화 기법이다.

세마포어는 철도 신호기에서 유래한 단어다. 신호기가 내려가 있을 때는 멈춤 신호로 간주해 잠시 멈추고, 신호기가 올라가 있으면 가도 좋다는 신호로 간주해 다시 움직인다. 프로세스는 멈춤 신호를 받고 잠시 기다렸다가 가도 좋다는 신호를 받으면 임계구역에 들어간다.

세마포어의 구현

세마포어는 하나의 전역 변수(S)와 두 개의 함수로 구현할 수 있다. 전역 변수 S는 사용 가능한 공유 자원의 개수를 나타낸다. wait() 함수(P 연산)는 기다려야 하는지 들어가도 좋은지 알려주는 역할을 하며, signal() 함수(V 연산)는 임계구역 앞에서 기다리는 프로세스에게 들어가도 좋다는 신호를 주는 역할을 한다.

class Semaphore {
    int value = 1; // 권한의 개수

    void acquire() {
        value--;
        if (value 
#include 

int ncount; // 스레드간 공유되는 자원
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* do_loop(void *data) {
    int i;
    for (i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex); // 잠금을 생성
        printf("loop1 : %d\n", ncount);
        ncount++;
        pthread_mutex_unlock(&mutex); // 잠금을 해제
        sleep(1);
    }
    return 0;
}

이 예제에서는 pthread_mutex_lock()과 pthread_mutex_unlock()을 사용하여 공유 자원인 ncount에 대한 동시 접근을 제어한다.

모니터 (Monitor)

모니터(Monitor)는 뮤텍스나 세마포어보다 더 고수준의 동기화 기법이다. 세마포어를 편리하게 사용하기 위해 인터페이스를 제공한 것이 모니터라고 할 수 있다. 모니터는 근본적인 고급 언어 구조물 중 하나로, Java, C# 등의 많은 프로그래밍 언어들이 모니터의 개념을 편입시켰다.

모니터는 공유 자원과 공유 자원에 접근하기 위한 인터페이스로 구성되어 있다. 모니터 내부에는 프로그래머가 정의한 일련의 연산자 집합을 포함하는 추상화된 데이터 형(ADT)이며, 모니터 내부에서 상호배제가 보장되어야 한다. 모니터 앞에는 상호 배제를 위한 큐가 있으며, 공유 자원을 이용하려 하는 프로세스는 이 큐에 삽입되어 인터페이스에 접근하게 된다.

모니터의 구조와 동작 원리

자바에서 모든 객체는 모니터를 가진다. 여러 스레드가 객체의 임계영역에 진입하려고 할 때 JVM은 모니터를 사용해 스레드 간 동기화를 제공한다. 자바의 모니터는 상호 배제(Mutual Exclusion)와 협력(Cooperation)이라는 두 가지 동기화 기능을 제공하며, 이를 위해 뮤텍스와 조건 변수를 사용한다.

모니터 내부에는 EntrySet(진입셋)과 WaitSet(대기셋)이라는 대기 자료 구조가 있다. EntrySet은 모니터의 Lock을 획득하기 위해 대기 중인 스레드를 모아 놓은 자료구조이고, WaitSet은 모니터의 조건 변수와 함께 사용하는 자료구조로 스레드들이 특정한 조건이 만족할 때까지 대기하고 있는 장소다.

조건 변수와 동기화

모니터는 상호 배제뿐만 아니라 실행 순서 제어를 위한 동기화도 제공하는데, 이는 조건 변수(Condition Variable)를 사용한다. 조건 변수에 호출될 수 있는 연산은 wait()와 signal() 뿐이다. x.wait()를 호출한 프로세스는 다른 프로세스가 x.signal()을 호출할 때까지 일시중지되며, x.signal()은 정확히 하나의 일시 중지 프로세스를 재개한다.

public class Count {
    private int val = 0;

    public synchronized void increase() {
        ++val;
    }

    public int getVal() {
        return val;
    }
}

위 예제에서 synchronized 키워드를 통해 모니터가 간접적으로 사용된다. synchronized 키워드가 붙은 increase 메서드는 멤버변수 val에 대해서 thread safe한 동작을 제공한다.

생각


이론으로만 알았다면 지루했겠지만 어느 정도 프로젝트를 진행해보고 다시금 배우니 흥미로웠고 이해도 더 빨랐다. 확실히 이런 방식이 아니라면 프로세스들은 원할하게 처리되지 않았을 것이다. 날이 갈수록 운영체제에 대해 더 깊이 이해하고 있는 것 같아 스스로가 나아가는 방향에 확신을 갖을 수 있게 됐다.

반응형