CS/운영체제

락(Lock), 컨디션 변수, 세마포어 정리

soo-dal 2024. 1. 26. 10:23

 

목차

1. 락(Lock) (뮤텍스, 스핀락)
2. 컨디션 변수(Condition Variable)
3. 세마포어(Semaphore)

 


1. 락(Lock) (뮤텍스, 스핀 락)

개념

멀티 스레드로 인해 여러 스레드가 같은 자원에 동시에 접근하는 일이 발생하며, 이로 인해 동기화 문제가 발생하게 된다. 이를 해결하기 위해 락(Lock) 이라는 메커니즘을 통해 접근을 제어한다. 

락(Lock) = 상호배제(Mutual Exclusion)를 통해 여러 스레드가 동시에 공유 자원에 접근하지 못하도록 하는 메커니즘

 

 

소스 코드의 임계영역(Critical Section)을 락으로 둘러서 그 임계영역이 하나의 원자 단위 명령어인 것 처럼 실행될 수 있도록 한다.

임계 영역(Critical Section) = 소스 코드 상에서 락으로 둘러싸진 영역. 이 영역은 스레드 하나만 실행된다.

//예시
lock(&m) // 락 획득
----------------------
임계 영역(Critical Section)
----------------------
unlock(&m) // 락 해제

 

 

기본 사용법

우선 락에 사용되는 변수인 락 변수를 먼저 선언하고, 해당 락 변수 값을 lock과 unlock 이라는 함수로 변경하여 락 획득을 시도하고 반환한다. lock 함수를 통해 락을 획득할 수도 있고, 다른 스레드가 사용하고 있어서 대기를 해야하는 경우도  발생한다. 락을 획득한 스레드는 임계 영역에 진입할 수 있고, 락을 획득하지 못한 스레드라면 해당 임계영역에 진입하지 못하고 대기해야한다.

lock_struct m; // 사용가능(available), 사용중(acquired) 중 하나를 가짐

lock(&m); // m -> acquired 로 변경(락 획득)
----------------------
임계 영역(Critical Section)
----------------------
unlock(&m); // m-> available 로 변경(락 해제)

 

뮤텍스(Mutex)

스레드 간 상호 배제(Mutual Exclusion) 기능을 제공하기 때문에 POSIX 라이브러리에서는 락을 Mutex 라고 부른다. 앞서 설명했던 Lock의 개념 및 사용법과 같다.

뮤텍스(Mutex) = 상호배제를 통해 한 스레드만 임계영역에 진입할 수 있도록 제어하는 메커니즘

 

 

스핀 락(Spin Lock)

이미 다른 스레드가 락을 획득해서 대기를 해야할 경우, while 문으로 계속 돌면서 락의 해제를 기다리는 락을 스핀 락(Spin Lock) 이라고 한다. 이때 while 문을 사용하여 명령어가 계속 실행되기 때문에 CPU를 계속 사용하게 된다.

스핀 락(Spin Lock) = while문을 돌면서 락의 해제를 대기하는 락

 

스핀 락 한계

1. 공정성 문제 
- 회전하여 대기하는 스레드 간 락의 획득 순위를 보장하지 않기 때문에, 계속 락을 획득하지 못하는 스레드가 발생할 수 있다.

2. CPU 사이클 낭비
- 대기시 while 문을 사용하기 때문에 계속해서 CPU를 사용해야한다.

 

 

그 외의 락

스핀 락이 계속해서 대기시 계속 CPU를 소모해야하다 보니 OS 마다 효율적인 락을 지원하기 위해 다른 방식들을 제공한다. 간단하게 어떤 방법들이 있는지 소개하고 넘어가겠다.

 

1. Solarise OS - part, unpark 함수를 사용하여 스레드를 잠자게 하고, 깨운다. 또한, 함수 사용시 Thread Id를 사용하여 공정성 문제를 해결한다.

 

2. Linux - Futex 라는 것을 사용한다. futex는 특정 물리 메모리 주소와 연결이 되어 있고, fuext마다 커널 내부에 큐를 가지고 있다.  futex를 사용하여 잠 자거나 깨어날 수 있다.

 

 

2. 컨디션 변수(Condition Variable)

스레드에서 특정 조건을 만족해야 실행을 계속 진행하고 싶을 때가 존재한다. 이 때 특정 조건이 만족할 때 까지 기다릴 수 있도록 하는 동기화 메커니즘이 컨디션 변수(Condition Variable) 이다. 부모, 자식 스레드가 존재할 때, 부모 스레드는 자식 스레드가 끝나야(특정 조건) 나머지를 진행하고 싶은 경우가 이에 해당한다.

컨디션 변수(Condition Variable) = 특정 조건이 만족할 때 까지 대기할 수 있도록 하는 동기화 메커니즘

 

 

사용 예시

int done = 0;
pthread_mutex_t m = PTHREAD_MUTEXT_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;

//자식 스레드에서 사용할 함수
void *child(void *arg){
    printf("child\n");
    
    phread_mutex_lock(&m);
    done=1;
    Pthread_cond_signal(&c) //잠자는 스레드를 깨운다
    Pthread_mutex_unlock(&m);
    
    return NULL;
}

void thr_join(){
    phread_mutex_lock(&m);
    while(done==0)
    	Pthread_cond_wait(&c) // 자식 스레드가 끝날때까지 잔다.
    Pthread_mutex_unlock(&m);
}

int main(int argc, char *argv[]){
    printf("parent : begin\n");
    pthread_t p;
    Phread_create(&p, NULL,child,NULL); //자식 스레드 실행
    
    thr_join();
    
    printf("parent: end\n");
    return 0;
}

 

 

3.  세마포어(Semaphore)

임계 영역에 특정 스레드만 접근할 수 있도록 하는 뮤텍스(Mutex)와 특정 조건을 만족할 때까지 대기하도록 하는 컨디션 변수(Condition Variable) 에 대해 살펴보았다. 1965년 다익스트라(최단 경로의 다익스트라 알고리즘)는 이 두 가지를 한번에 할 수 있는 세마포어(Semaphore)라는 메커니즘을 발표했다. 

세마포어(Semaphore) = 정수 객체를 활용하여 뮤텍스와 컨디션 변수의 기능을 가능하게 하는 메커니즘

 

 

구성

1. 세마포어 변수(s) - 세마포어에서 사용하는 변수이며, 해당 값을 기준으로 스레드를 실행 또는 대기시킨다.

2. p 작업(sem_wait) - 호출 시 s의 값을 1 감소시킨 후, s 값이 0 이상이면 실행시키고 음수이면 대기시킨다.

3. v 작업(sem_post) - 호출 시 s의 값을 1 증가시킨 후, s 값이 0 이상이면 대기 중인 스레드 중 하나를 깨운다.

 

 

뮤텍스로서의 세마포어(바이너리 세마포어)

초기 세마포어를 소개할 때 세마포어를 뮤텍스로 활용할 수 있다고 했다. 어떻게 세마포어를 통해 뮤텍스로 사용할 수 있는지 알아보자.

sem_t s;
sem_init(&s, 0, X); // X는 세마포어 초기화 값, 뮤텍스로 활용시 1으로 초기화
sem_wait(&s); // P 작업
---
임계영역
---
sem_post(&s); // V 작업

위의 코드 처럼 처음에는 세마포어의 값을 초기화 한 후, p 작업과 v 작업으로 임계영역을 감싸서 뮤텍스로 활용할 수 있다. 이때, 세마포어의 초기값을 어떻게 설정하느냐가 중요한데, 뮤텍스로 활용하기 위해서는 X=1 로 초기화 해야한다.

 

 

위의 코드를 통해 락을 획득한 스레드가 없는 경우 어떻게 동작하는지 코드와 주석을 통해 확인해보자.

sem_t s;
sem_init(&s, 0, 1); 

// 현재 세마포어의 값(s) = 1
sem_wait(&s); // s의 값을 1 감소시켜서 0이 되며, 해당 값이 0 이상이기 때문에 계속 실행한다.
---
임계영역       //해당 영역이 임계영역에 진입하여 명령어를 수행한다.
---
sem_post(&s); // 명령어 수행이 완료되었다면 s의 값을 1 증가시키고, 대기하고 있는 스레드를 실행시킨다.

 

 

이제 특정 스레드A가 락을 획득한 상태라고 가정할 때, 특정 스레드 B가 락을 획득하려할 때 어떤 일이 발생하는지 알아보자.

sem_t s;
sem_init(&s, 0, 1); 

// 특정 스레드A 가 이미 락을 획득하여 임계영역에 들어가있다. 즉, p 작업을 수행하여 s의 값이 0인 상태이다.
sem_wait(&s); // s의 값을 1 감소시켜서 -1 이 되며, 해당 값이 음수이기 때문에 대기한다.
---			 // 추후 A가 락을 해제한다면 락을 획득하게된다.
임계영역       
---
sem_post(&s);

 

 

위와 같이 특정 스레드A가 이미 락을 획득한 상태에서 스레드 B가 락 획득을 시도할 때, 세마포어의 값이 1 감소하며 대기를 하는 상황이 발생한다. 만약 다수의 스레드가 락을 획득하려는 경우 스레드의 개수만큼 세마포어의 값이 감소한다. 즉, 세마포어의 값 (-n) = 대기하는 스레드의 개수 n개 라는 특징을 갖는다.

 

 

컨디션 변수로서의 세마포어

세마포어를 통해 뮤텍스 뿐 아니라 컨디션 변수로서도 사용할 수 있다. 특정 조건이 될 때까지 대기를 시킬 수 있는데 어떻게 활용하는지 예시로 확인해보자.

sem_t s;
void *child(void *arg){
    printf("child\n");
    sem_post(&s) // 세마포어 V 작업 실행
    return NULL;
}

int main(int argc,char *argv[]){
    sem_init(&s, 0, X); // 컨디션 변수로 활용시 X=0 이어야한다.
    printf("parent: begin\n");
    
    //자식 스레스 생성 및 실행
    pthread_t c;
    Pthread_create(c, NULL, child, NULL);
    
    sem_wait(&s); // 세마포어 P 작업 수행 -> 자식 스레드가 끝날 때까지 대기
    
    printf("parent: end\n");
    return 0;
}

 

 

위의 작업에서 두가지 상황이 있을 수 있는데, 첫번째로 부모 스레드가 먼저 P 작업을 수행하는 경우와, 자식 스레드가 V작업을 먼저 수행하는 경우이다. 

 

부모 스레드에서 먼저 P작업을 수행하는 경우

1. P작업으로 인해 s 값 변경: 0 -> -1 , 부모 스레드는 대기 상태로 전환

2. 자식 스레드에서 V작업을 통해 s 값 변경 : -1 -> 0 , 부모 스레드가 대기에서 풀리며 실행 완료

 

자식 스레드에서 V 작업을 먼저 수행하는 경우

1. V작업으로 인해 s 값 변경 : 0 -> 1

2. 부모 스레드에서 P 작업을 통해 s 값 변경 : 1 -> 0, 0이상이므로 계속 진행

 

위와 같이 세마포어를 컨디션 변수로 활용하여 특정 조건이 발생할 때까지 대기 시킬 수 있다.

 

 

요약

뮤텍스(Mutex, ≒ 락)
개념 - 상호배제를 통해 임계영역에 하나의 스레드만 진입할 수 있도록 제어하는 메커니즘

스핀 락(Spin Lock)
개념 - 락 획득을 위해 대기할 때, 루프를 돌면서 대기하는 뮤텍스 방식
한계 - 공정성 문제가 있으며, 루프를 계속 돌기 때문에 CPU 자원을 소모
해결 방안 - 스레드를 재우거나, 큐를 활용

컨디션 변수(Condition Variable)
개념 - 특정 조건이 발생할 때까지 대기할 수 있도록 하는 메커니즘
예시 - 자식 스레드가 끝날 때까지 부모 프로세스 대기

세마포어(Semaphore)
개념 - 정수 변수를 활용하여 뮤텍스, 컨디션 변수의 동기화 기능을 제공하는 메커니즘
구성 
1. 세마포어 값(s)
2. P 작업 - s를 1 감소 -> 음수이면 스레드 대기
3. V 작업 - s를 1 증가 -> 대기중인 프로세스 중 하나 깨움

바이너리 세마포어의 경우, 세마포어의 절댓값 = 대기하는 스레드 개수

 

 

[참고 링크]

Remzi H. Arpaci-Dusseau,Andrea C. Arpaci-dusseau 공저, 「운영체제 아주 쉬운 세가지 이야기」, 홍릉, 2020

https://github.com/gyoogle/tech-interview-for-developer?tab=readme-ov-file