이 글은 윤성우의 TCP/IP 소켓 프로그래밍 책을 참고하였습니다.

순서

I. Mutex

II. Semaphore


I. Mutex

뮤텍스(Mutex)는 앞서 말한 것과 같이 스레드를 동기화 하기 위해 사용하는 방법이다.

각 스레드가 동시에 동일한 주소에 접근하는 현상을 예방하기 위해

한 스레드가 주소에 접근했을 때 다른 스레드의 주소 접근을 막는 것이다.

자물쇠라고 생각하면 편하다.
화장실에 들어갈 때 문을 잠그고 나올 때 여는...

Mutex관련 함수는 다음 4가지가 있다.
1. pthread_mutex_init / pthread_mutex_destroy :  생성과 초기화
2. pthread_mutex_lock / pthread_mutex_unlock : 잠그고 열기


1. Mutex의 생성과 초기화 함수

#include <pthread.h>

int pthread_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_destory(pthread_mutex_t *mutex);
    --> 성공 시 0, 실패 시 0 이외의 값 반환
    
    ㄴ. mutex : 생성시 - 뮤텍스의 참조 값 저장을 위한 변수의 주소 값 전달.
                소멸시 - 소멸하고자 하는 뮤텍스의 참조 값을 저장하고 있는 변수의 주소 값 전달.
    
    ㄴ. attr  : 생성하는 뮤텍스의 특성정보를 담고 있는 변수의 주소 값 전달.
                별도 특성 지정안하려면 NULL전달.

인자 1. pthread_mutex_t* mutex

뮤텍스의 참조 값 저장을 위한 변수의 주소 값 인자이다.
이 인자는 프로세스 내에서 pthread_mutex_t 자료형의 변수를 선언해서 사용해준다.
초기화할 때도 이 변수의 주소를 전달하여 내부 참조 값을 초기화시킨다.

인자 2. const pthread_mutexattr_t* attr

NULL 전달하면 기본 특성을 가진다.

NULL을 사용할 때는 다음과 같이 변수만 선언하고 init함수 안 해도 된다.

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

그러나 이 책에서는 오류 발생의 확인이 어려워 가급적 함수를 모두 사용할 것을 권장한다.


2. Mutex의 잠금과 해제 함수

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
    --> 성공 시 0, 실패 시 0 이외의 값 반환

인자 1. pthread_mutex_t* mutex

위에서 선언하고, 생성했던 pthread_mutex_t 자료형의 변수 주소 값을 전달한다.

이 함수를 어떻게 사용해야 할까?

pthread_mutex_lock(&user_mutex);

// 임계영역의 코드 작성

// 임계영역의 코드 작성 끝

pthread_mutex_unlock(&user_mutex);

다음과 같이 원하는 임계영역의 코드를 작성할 때 양 끝에 잠금과 해제를 알리면 된다.

잠겨있을 때는 다른 스레드는 블로킹 상태에 놓이게 된다.


예제코드

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

#define NUM_THREAD 100

void* thread_A(void* arg); // ++하는 스레드
void* thread_B(void* arg); // --하는 스레드

int critical_val{0}; // 임계영역 변수
pthread_mutex_t mutex; // 외부 함수에서 사용할 것이므로 전역으로 선언

int main()
{
    pthread_t thread_id[NUM_THREAD]; // 스레드 id배열 정의

    pthread_mutex_init(&mutex,NULL); // 뮤텍스 생성
    
    // 스레드 생성
    for(int i=0;i<NUM_THREAD;i++)
    {
        if(i%2) pthread_create(&(thread_id[i]), NULL, thread_A, NULL);
        else pthread_create(&(thread_id[i]), NULL, thread_B, NULL);
    }
    
    // 스레드-프로세스 동기화
    for(int i=0;i<NUM_THREAD;i++)
        pthread_join(thread_id[i],NULL);
    
    printf("result : %d\n",cretical_val);
    
    // 뮤텍스 다 쓰면 파괴시켜줌
    pthread_mutex_destory(&mutex);
    return 0;
}

// ++하는 스레드
void* thread_A(void* arg)
{
    pthread_mutex_lock(&mutex)
    for(int i=0;i<5000;i++)
        critical_num++;
    pthread_mutex_unlocK(&mutex);
    
    return NULL;
}

// --하는 스레드
void* thread_B(void* arg)
{
    pthread_mutex_lock(&mutex)
    for(int i=0;i<5000;i++)
        critical_num--;
    pthread_mutex_unlocK(&mutex);
    
    return NULL;
}

실행결과
result : 0

 


II. Semaphore

세마포어는 뮤텍스랑 굉장히 비슷하다.

하지만, 순서를 결정해주는 성격을 보이기 때문에 3개 이상의 멀티 스레드에서 주로 사용한다.

세마포어에는 2개가 있다.
1. 이진 세마포어 (Binary Semaphore)
: 0과 1만 사용하여 동기화 = 뮤텍스

2. 카운팅 세마포어 (Counting Semaphore)
: 점점 증가하는 수로 여러 스레드의 여러 자원을 동기화함.

이번 글에서는 이진 세마포어를 위주로 설명할 것이다.
(추가적인 구현 부분)

세마포어도 관련 함수도 다음 4가지가 있다.
1. sem_init / sem_destroy :  생성과 초기화
2. sem_post / sem_wait : 잠그고 열기

 


1. 세마포어의 생성과 초기화

#include <semaphore.h>

int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem)
    --> 성공 시 0, 실패 시 0이외의 값 반환
    
    ㄴ. sem     : 세마포어의 참조 값 저장 및 삭제를 위한 변수의 주소 값
    
    ㄴ. pshared : 0 이외의 값 전달 시, 둘 이상의 프로세스에 의해 접근 가능한 semaphore생성
                  0 전달 시, 하나의 프로세스 내에서만 접근 가능한 semaphore 생성
                  우리는 하나의 프로세스에서 다중 스레드를 구현하므로 0
    
    ㄴ. value   : 생성되는 semaphore의 초기 값 지정

sem_init을 호출하게 되면 운영체제에 의해 "세마포어 오브젝트"가 만들어지고,
여기에는 "세마포어 값"이라는 정수가 기록된다.

인자 1. sem_t* sem

mutex처럼 sem_t의 자료형을 가지는 세마포어 변수의 주소 값을 전달한다.

이 변수에 세마포어의 참조 값이 저장되고 삭제된다.

인자 2. int pshared

보통 하나의 프로세스에서 다중 스레드를 구현할 때는 0을 인자로,
다중 프로세스에서 접근하고자 하는 세마포어를 구현할 때는 0 이외의 값을 인자로 넣는다.

인자 3. unsigned int value

세마포어가 생성될 때 초기 값을 가지는데, 이 초기 값을 지정해준다.
이 초기 값에 관한 내용은 다음 함수에서 설명할 것이다.

 


2. 세마포어 잠그고 열기

#include <semaphore.h>

int sem_post(sem_t* sem);
int sem_wait(sem_t* sem);
    --> 성공 시 0, 실패 시 0 이외의 값 반환

인자 1. sem_t* sem

sem_init을 위해 사용한 sem_t 자료형의 변수 주소를 인자로 넣어준다.

sem_post를 호출하면 위에서 말한 세마포어 값이 1 증가한다.
sem_wait을 호출하면 위에서 말한 세마포어 값이 1 감소한다.

*세마포어 중요한 성질과 원리*

세마포어 값은 0보다 작아질 수 없다.

따라서 0인 상태에서 sem_wait이 호출되면
호출한 스레드는 함수가 반환되지 않아 블로킹에 걸린다!!

하지만 블로킹이 걸렸을 때, 다른 스레드가 sem_post를 호출해주면
세마포어의 값이 1이 되므로 블로킹에서 빠져나가게 된다.

형광 칠한 부분이 스레드의 순서를 결정짓는 핵심 포인트다.

이 함수를 어떻게 사용해야 할까?

sem_wait(&sem); // 값을 0으로

//임계영역시작

//임계영역 끝

sem_pose(&sem); // 값을 1로

 

sem_wait을 호출하게 되면 임계 영역에 진입한 thread가 sem_post를 호출하기 전까지
다른 스레드에서 임계 영역의 진입이 허용되지 않는다.

세마포어는 0과 1의 값을 오고 가는데, 이런 특징으로 Binary semaphore라는 이름이 붙여졌다.


예제 코드

" A가 사용자로부터 숫자를 입력받아 전역 변수 num에 저장하면,
B는 값을 가져다가 sum에 누적해나간다."

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

void* thread_input(void* arg);
void* thread_sum(void* arg);

static int input_num;

static sem_t sem_zero;
static sem_t sem_one;

int main()
{
    pthread_t id_t1, id_t2;
    
    sem_init(&sem_zero, 0, 0); // 0으로 시작하는 세마포어
    sem_init(&sem_one , 0, 1); // 1로 시작하는 세마포어
    
    pthread_create(&id_t1, NULL, thread_input, NULL); //스레드 생성
    pthread_create(&id_t2, NULL, thread_sum  , NULL);
    
    pthread_join(&id_t1,NULL); //스레드 - 프로세스 동기화
    pthread_join(&id_t2,NULL);
    
    sem_destroy(&sem_zero); // 끝나면 세마포어 제거해줘야함
    sem_destroy(&sem_one);
    
    return 0;
}

void* thread_input(void* arg)
{
    for(int i=0;i<5;i++)
    {
        fputs("Input num : ", stdout);
        
        sem_wait(&sem_one); // sem_one을 wait시켜서 0만듦
        scanf("%d",&num); // 주소접근해 저장
        sem_post(&sem_zero); // sem_zero를 post시켜서 1만듦
    }
    return NULL;
}

void* thread_sum(void* arg)
{
    int sum;
    for(int i=0;i<5;i++)
    {
        sem_wait(&sem_zero); // sem_zero를 wait해서 blocking상태,
                             // input에서 1로되고 0으로 변경되며 blocking풀림
        sum += num;
        sem_post(&sem_one); // 0이 된 sem_one을 1로 만들면서 post함
    }
    printf("total Sum : %d\n",sum);
    return NULL;
}

실행결과

Input num : 1
Input num : 2
Input num : 3
Input num : 4
Input num : 5
total Sum : 15

 

세마포어가 두 개가 필요했는데,
이 부분이 이해가 가지 않는다면 세마포어 처리과정을 생각하며 코드를 찬찬히 보자.

먼저 입력이 끝나고, 누적이 되기 때문에 input->sum->input->sum 순서로 계속 반복되어야 한다.

 

 

+ Recent posts