이 글은 윤성우의 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 순서로 계속 반복되어야 한다.
'Programming > Network(C++)' 카테고리의 다른 글
[Network][Thread] 멀티스레드(6)_윈도우 스레드 구현(Window) (0) | 2020.12.23 |
---|---|
[Network][Thread] 멀티스레드(5)_정리 및 멀티스레드 서버 구현(Linux) (0) | 2020.12.22 |
[Network][Thread] 멀티스레드(3)_동시접근의 문제점 (0) | 2020.12.21 |
[Network][Thread] 멀티스레드(2)_스레드의 생성 및 실행(pthread)(Linux) (0) | 2020.12.20 |