[Linux] 프로세스 동기화의 기본: 뮤텍스와 세마포어의 차이와 활용 방법

Ma_Seokjae·2024년 6월 19일
0
post-thumbnail

병행 프로그래밍에서 동시에 여러 프로세스나 스레드가 접근할 때 발생할 수 있는 경합 상태(race condition)방지하는 방법을 알아보도록 하겠습니다

Race condition을 방지하기 위해 충족되어야 하는 3가지 조건이 있습니다.

  1. Mutual Exclusuin (상호 배제)

    • 상호 배제는 동시에 하나의 프로세스만이 임계 구역에 진입할 수 있도록 보장하는 조건입니다.
    • 여기서, 임계 구역(Critical Section)은 공유 자원을 접근하거나 수정하는 코드 영역입니다.
  2. Progress (진행)

    • 진행 조건은 임계 구역에 들어가기를 원하는 프로세스가 있으면, 반드시 어느 한 프로세스가 임계 구역에 들어갈 수 있어야 한다는 조건입니다.
    • 이 조건은 임계 구역에 진입하지 않으려는 프로세스가 임계 구역에 진입하고자 하는 프로세스의 방해를 하지 않도록 보장합니다.
  3. Bounded Waiting (한정 대기)

    • 한정 대기 조건은 특정 프로세스가 임계 구역에 들어가기를 원하면, 다른 프로세스들이 임계 구역에 들어가는 횟수에 상한을 두는 조건입니다.
    • 즉, 어떤 프로세스가 임계 구역에 들어가기 위해 무한정 기다리지 않도록 보장합니다.

이 세 가지 조건을 충족시키기 위해, Mutex(뮤텍스)와 Semaphore(세마포어)와 같은 동기화 도구를 사용하여 구현할 수 있습니다. 이제 이 두 개념과 구현 예시를 통해, 코드에서 Race Condition을 어떻게 방지하는지 자세히 알아보겠습니다.


Mutex

뮤텍스는 한 번에 하나의 스레드만이 자원에 접근할 수 있도록 보장합니다. C언어에서 'pthread_mutex_t' 타입을 사용하여 이를 구현합니다.

1. pthread_mutex_init()

뮤텍스 객체를 생성하고 초기화합니다. 기본 속성 또는 사용자 정의 속성으로 초기화할 수 있습니다.

  • int pthread_mutex_init(pthread_mutex_t mutex, const pthread_mutexattr_t attr)

  • 파라미터 정보:

    • mutex: 초기화할 뮤텍스 객체에 대한 포인터
    • attr: 뮤텍스 속성 객체에 대한 포인터. 기본 속성을 사용하려면 'NULL'로 설정

    예시:

    pthread_mutex_t lock;
    pthread_mutex_init(&lock, NULL);

2. pthread_mutex_destroy

뮤텍스 객체를 파괴하고 관련된 리소스를 해제합니다. 초기화된 뮤텍스를 해제하여 자원을 반환합니다.

  • int pthread_mutex_destroy(pthread_mutex_t *mutex)

  • 파라미터 정보:

    • mutex: 해제할 뮤텍스 객체에 대한 포인터

    예시:

    pthread_mutex_destroy(&lock)

3. pthread_mutex_lock()

해당 뮤텍스를 잠금으로써 다른 스레드가 임계 구역에 접근하지 못하게 합니다. 뮤텍스가 이미 잠겨있으면 스레드는 블록됩니다.

  • int pthread_mutex_lock(pthread_mutex_t *mutex)

  • 파라미터 정보:

    • mutex: 잠그려는 뮤텍스 객체에 대한 포인터. 해당 뮤텍스가 잠길 때까지 호출된 스레드는 블록된다.

    예시:

    pthread_mutex_lock(&lock);

4. pthread_mutex_unlock()

잠겨 있던 뮤텍스를 해제하여 다른 스레드가 임계 구역에 접근할 수 있게 합니다.

  • int pthread_mutex_unlock(pthread_mutex_t *mutex)

  • 파라미터 정보:

    • mutex: 잠금을 해제할 뮤텍스 객체에 대한 포인터

    예시:

    pthread_mutex_unlock(&lock);

정리

#include <pthread.h>
#include <stdio.h>

pthread_mutex_t lock;

void* thread_func(void* arg);

int main() {
    pthread_t t1, t2;
    int id1 = 1, id2 = 2;
	
    // 뮤텍스 초기화
    pthread_mutex_init(&lock, NULL);
    // 두 개의 스레드 생성
    pthread_create(&t1, NULL, thread_func, &id1);
    pthread_create(&t2, NULL, thread_func, &id2);
	
    // 두 스레드가 종료될 때까지 기다림
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_mutex_destroy(&lock);

    return 0;
}

void* thread_func(void* arg) {
	// 뮤텍스를 잠금
    pthread_mutex_lock(&lock);
    // 임계 구역 진입
    printf("Thread %d in critical section\n", *(int*)arg);
    // 뮤텍스 잠금 해제
    pthread_mutex_unlock(&lock);
    return NULL;
}

1. pthread_mutex_init(&lock, NULL):
lock이라는 뮤텍스를 초기화

2. pthread_create(&t1, NULL, thread_func, &id1):
첫 번째 스레드를 생성하여 thread_func을 실행하고, 스레드 식별자를 통해 스레드에 id1 값을 전달

3. pthread_create(&t2, NULL, thread_func, &id2):
두 번째 스레드를 생성하여 thread_func을 실행 후 스레드 식별자를 통해 스레드에 id2 값을 전달

4. pthread_join(t1, NULL):
첫 번째 스레드가 종료될 때까지 대기

5. pthread_join(t2, NULL):
두 번째 스레드가 종료될 때까지 대기

6. pthread_mutex_destroy(&lock):
lock 뮤텍스를 소멸시킴

thread_func 함수는 뮤텍스를 잠금으로 설정한 후, 임계 구역에 진입하여 "Thread X in critical section"을 출력하고, 뮤텍스 잠금을 해제합니다. 이로 인해 두 스레드는 동시에 임계 구역에 진입하지 못하고, 하나씩 차례로 임계 구역에 진입합니다.


Semaphore

세마포어는 자원에 대한 접근을 신호화(signaling)하고 제어하기 위해 사용됩니다. 이름 있는 세마포어와 이름 없는 세마포어의 두 가지 종류가 있습니다.

Named Semaphores

1. sem_open()

지정된 이름을 가진 세마포어를 생성하거나 열어서 세마포어 객체에 대한 포인터를 반환합니다. 필요한 경우 초기 값을 설정합니다.

  • sem_t sem_open(const char name, int oflag)

  • 파라미터 정보:

    • name: 세마포어의 이름
    • oflag: 세마포어가 생성될 때 사용하는 플레그. 'O_CREAT'와 같은 플래그를 사용

    예시:

    sem_t *sem = sem_open("/example_sem", O_CREAT, 0644, 1);

2. sem_close()

세마포어 객체를 닫고, 프로그램이 더 이상 해당 세마포어를 사용하지 않음을 나타냅니다.

  • int sem_close(sem_t *sem)

  • 파라미터 정보:

    • sem: 닫을 세마포어 객체에 대한 포인터

    예시:

    sem_close(sem);

세마포어 이름과 관련된 시스템 자원을 해제합니다. 세마포어가 더 이상 사용되지 않도록 합니다.

  • int sem_unlink(const char *name)

  • 파라미터 정보:

    • name: 삭제할 세마포어의 이름

    예시:

    sem_unlink("/example_sem");

Unnamed Semaphores

1. sem_init()

세마포어 객체를 생성하고 초기 값으로 초기화합니다. 프로세스 간 또는 스레드 간 공유 여부를 설정합니다.

  • int sem_init(sem_t *sem, int pshared, unsigned int value)

  • 파라미터 정보:

    • sem: 초기화할 세마포어 객체에 대한 포인터
    • pshared: 세마포어가 프로세스 간에 공유될지 여부 (0은 스레드 간 공유, 1은 프로세스 간 공유)
    • value: 세마포어의 초기 값

    예시:

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

2. sem_destroy()

세마포어 객체를 파괴하고 관련된 리소스를 해제합니다.

  • int sem_destroy(sem_t *sem)

  • 파라미터 정보:

    • sem: 해제할 세마포어 객체에 대한 포인터

    예시:

    sem_destroy(&sem);

3. sem_wait()

세마포어 값을 감소시키며, 값이 0 이하이면 스레드는 블록됩니다. 자원이 사용 가능해질 때까지 대기합니다.

  • int sem_wait(sem_t *sem)

  • 파라미터 정보:

    • sem: 감소할 세마포어 객체에 대한 포인터. 세마포어 값이 0이면 호출된 스레드는 블록

    예시:

    sem_wait(&sem);

4. sem_post()

세마포어 값을 증가시켜 대기 중인 스레드 중 하나를 깨워서 자원에 접근할 수 있게 합니다.

  • int sem_post(sem_t *sem)

  • 파라미터 정보:

    • sem: 증가할 세마포어 객체에 대한 포인터

    예시:

    sem_post(&sem);

정리

#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>

sem_t sem;

void* thread_func(void* arg);

int main() {
    pthread_t t1, t2;
    int id1 = 1, id2 = 2;
	
    // 세마포어 초기화 (초기값 1)
    sem_init(&sem, 0, 1);
    // 두 개의 스레드 생성
    pthread_create(&t1, NULL, thread_func, &id1);
    pthread_create(&t2, NULL, thread_func, &id2);
	
    // 두 스레드가 종료될 때까지 기다림
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    sem_destroy(&sem);

    return 0;
}

void* thread_func(void* arg) {
	// 세마포어 대기
    sem_wait(&sem);
    // 임계 구역 진입
    printf("Thread %d in critical section\n", *(int*)arg);
    // 세마포어 해제
    sem_post(&sem);
    return NULL;
}

1. sem_init(&sem, 0, 1):
sem이라는 세마포어를 초기화, 초기값은 1

2. pthread_create(&t1, NULL, thread_func, &id1):
첫 번째 스레드를 생성하여 thread_func을 실행. 스레드 식별자를 통해 스레드에 id1 값을 전달

3. pthread_create(&t2, NULL, thread_func, &id2):
두 번째 스레드를 생성하여 thread_func을 실행. 스레드 식별자를 통해 스레드에 id2 값을 전달

4. pthread_join(t1, NULL):
첫 번째 스레드가 종료될 때까지 대기

5. pthread_join(t2, NULL):
두 번째 스레드가 종료될 때까지 대기

6. sem_destroy(&sem):
sem 세마포어를 소멸시킴

thread_func 함수는 세마포어를 대기(sem_wait) 상태로 설정한 후, 임계 구역에 진입하여 "Thread X in critical section"을 출력하고, 세마포어를 해제(sem_post)합니다. 이로 인해 두 스레드는 동시에 임계 구역에 진입하지 못하고, 하나씩 차례로 임계 구역에 진입합니다.


Mutex & Semaphore 차이점

Mutex

  • 한 번에 하나의 스레드만 자원에 접근할 수 있도록 보장
  • 같은 프로세스 내의 스레드를 동기화할 때 주로 사용
  • 상호 배제를 보장하는 데 세마포어보다 경량화되고 빠름

Semaphore

  • 자원에 대한 접근을 신호화하고 제어
  • 리소스 풀 관리 (ex: 제한된 수의 자원 관리)
  • 이름 있는 세마포어를 사용해서 프로세스 간 동기화 가능
  • 이름 없는 세마포어를 사용해서 같은 프로세스 내에서 동기화 가능

선택 기준?

  • Mutex를 사용할 때:

    • 상호 배제를 통해 임계 구역을 보호해야 할 때
    • 같은 프로세스 내에서 동기화를 해야 할 때
  • Semaphore를 사용할 때:

    • 유한한 수의 자원에 대한 접근을 동기화해야 할 때
    • 스레드나 프로세스 간의 신호를 구현해야 할 때
    • 프로세스 간 동기화를 위해 이름 있는 세마포어를 사용해야 할 때
profile
Why not change the code?

0개의 댓글

관련 채용 정보