우당탕탕 개발일지
[C언어] 동기화 API (feat. Rwlock API 사용해 은행 잔고 프로그램 구현하기) 본문
[C언어] 동기화 (feat. Mutex API 사용해 은행 잔고 프로그램 구현하기)
1. Race Condition (경쟁 상태) 둘 이상의 실행 주체가 동시에 하나의 공유 자원에 접근하려고 경쟁할 때 발생하는 상태이다. 프로세스마다 실행 속도가 달라서 예상치 못한 결과를 초래할 수 있다.
uj0791.tistory.com
Rwlock API
✅ 읽기-쓰기 락 초기화 방법
1. 정적으로 할당된 읽기-쓰기 락 초기화 → 오류 검사가 수행되지 않는다.
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
✅ pthread_rwlock_init()
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
- 읽기-쓰기 락 객체를 초기화하는데 사용하는 함수이다.
- rwlock : 변수를 선언하여 첫 번째 인자에 주소값을 넘겨 초기화할 수 있다.
- 한 번 초기화된 락은 재초기화 없이 여러 번 재사용 가능하다.
- attr : 읽기-쓰기 락 객체의 속성을 지정하며, NULL을 지정하면 기본 속성으로 설정된다.
✅ pthread_rwlock_destory()
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
- rwlock이 가리키는 읽기-쓰기 락 객체를 소멸시키고, 해당 락에서 사용하던 모든 자원을 해제한다.
- rwlock을 잡고 있는 스레드가 있을 때 pthread_rwlock_destroy()를 호출하는 경우, EBUSY 에러를 반환하도록 구현하는 것이 권장된다.
✅ 반환값
- 성공시 0을, 실패시 오류 번호를 리턴한다.
| ERROR | 설명 |
| EAGAIN | 시스템 자원이 부족하여 더 이상의 락을 초기화할 수 없음 |
| ENOMEM | 읽기-쓰기 락을 초기화 하기에 메모리가 부족하다. |
| EPERM | 호출자가 해당 작업을 수행할 권한이 없음 |
✅ pthread_rwlock_rdlock()
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
- rwlock에 대해 읽기 락을 적용한다.
- 호출 스레드는 다음 조건이 모두 충족될 경우에만 읽기 락을 얻는다.
- 다른 스레드가 쓰기 락을 보유하고 있지 않아야 한다.
- 쓰기 락을 기다리는 스레드가 없어야 한다.
- 하나의 스레드가 동일한 rwlock에 대해 여러 번 읽기 락을 설정하는 것은 허용되지만, 해당 횟수만큼 정확히 pthread_rwlock_unlock()을 호출하여 해제해야 한다.
✅ pthread_rwlock_wrlock()
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
- rwlock에 대해 쓰기 락을 적용한다.
- 해당 락이 이미 점유 중일 경우, 호출한 스레드는 블록되어 락이 해제될 때까지 기다린다.
✅ pthread_rwlock_unlock()
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
- 보유 중인 읽기-쓰기 객체를 파기한다.
- read lock을 해제하는 경우
- 해당 객체에 다른 읽기 락이 남아있다면, 락 객체는 계속 읽기 락 상태를 유지한다.
- 마지막 읽기 락을 해제하는 경우, 완전히 해제된 상태가 된다.
- write lock을 해제하는 경우, 락 객체는 해제된 상태가 된다.
- 락이 해제되면서 해당 락에 블록된 스레드가 있는 경우, 스케줄링 정책에 따라 결정된다.
- 같은 우선순위인 경우, read lock 보다 write lock을 우선한다.
✅ 반환값
- 성공시 0을, 실패시 오류 번호를 리턴한다.
🔷 rwlock lock을 이용한 은행 잔고 프로그램
- 은행 잔고는 전역 변수로 선언
- 은행 잔고를 조회하는 스레드는 3개 존재하고, 각 스레드는 은행 잔고를 1초/0.5초/0.2초에 한번씩 조회하여 출력하도록 스레드 구현
- 은행 잔고를 업데이트하는 스레드는 2개 존재하고, 1개 스레드는 은행 잔고를 0.1초에 1,000원씩 증가, 1개 스레드는 은행 잔고를 0.15초에 500원씩 감소하도록 구현
- 메인 스레드는 은행 잔고가 10,000원 이상이 되면 모든 스레드를 종료하고 프로그램 종료하도록 함
- 스레드 기동 순서를 다음과 같은 순서가 보장되도록 구현
- 은행 잔고 조회 스레드(1초->0.5초->0.2초 조회 스레드 순)
- 은행 잔고 업데이트 스레드(증가시키는 스레드 → 감소 시키는 스레드 순)
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#define READ_THREAD 3
#define UPDATE_THREAD 2
#define TOTAL_THREAD 5
#define TARGET_BALANCE 10000
pthread_rwlock_t rwlock;
int balance = 0, flag = 0;
typedef struct update_rwlock_t {
float time;
int amount;
} update_rwlock_t;
void *read_balance(void *arg);
void *update_balance(void *arg);
int main() {
if(pthread_rwlock_init(&rwlock, NULL) != 0) {
printf("[ERRPR] rwlock_init: %s\n", strerror(errno));
return -1;
}
float read_time[] = {1.0f, 0.5f, 0.2f};
update_rwlock_t update_info[] = {{0.1f, 1000}, {0.15f, -500}};
pthread_t p_thread[TOTAL_THREAD];
memset(p_thread, -1, sizeof(p_thread));
int i, ret;
for(i = 0; i < READ_THREAD; i++) {
ret = pthread_create(&p_thread[i], NULL, read_balance, &read_time[i]);
if(ret != 0) {
printf("[ERROR] pthread_create: %s\n", strerror(errno));
return -1;
}
}
for(i = 0; i < UPDATE_THREAD; i++) {
ret = pthread_create(&p_thread[i + READ_THREAD], NULL, update_balance, &update_info[i]);
if(ret != 0) {
printf("[ERROR] pthread_create: %s\n", strerror(errno));
return -1;
}
}
while(1) {
pthread_rwlock_rdlock(&rwlock);
if(balance >= TARGET_BALANCE) {
flag = 1;
pthread_rwlock_unlock(&rwlock);
break;
}
pthread_rwlock_unlock(&rwlock);
}
for(i = 0; i < TOTAL_THREAD; i++) {
if(p_thread[i] == -1) continue;
if(pthread_join(p_thread[i], NULL) != 0) {
printf("[ERROR] pthread_join: %s\n", strerror(errno));
}
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
void *read_balance(void *arg){
if(arg == NULL) {
printf("[ERROR] read_balance: arg is NULL\n");
pthread_exit((void *)(intptr_t) -1);
}
float *time = (float *)arg;
while(!flag) {
pthread_rwlock_rdlock(&rwlock);
printf("%.2f 조회 스레드 실행\n", *time);
printf("현재 은행 잔고는 %d원입니다.\n", balance);
pthread_rwlock_unlock(&rwlock);
usleep((*time) * 1000000);
}
pthread_exit((void *)(intptr_t) 0);
}
void *update_balance(void *arg) {
if(arg == NULL) {
printf("[ERROR] update_balance: arg is NULL\n");
pthread_exit((void *)(intptr_t) -1);
}
int cur_money;
update_rwlock_t *info = (update_rwlock_t *)arg;
float time = info->time;
int amount = info->amount;
while(!flag) {
pthread_rwlock_wrlock(&rwlock);
if(balance + amount < 0) {
printf("은행 잔고가 부족합니다.\n");
} else {
balance += amount;
}
usleep(time * 1000000);
pthread_rwlock_unlock(&rwlock);
}
pthread_exit((void *)(intptr_t) 0);
}
Semaphore API
✅ sem_init()
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 세마포어는 기본적으로 정수 타입의 상태값을 가지는 객체이다.
- sem : 가리키는 주소에 있는 이름 없는 세마포어를 초기화한다.
- pshared : 세마포어를 프로세스의 스레드 간에 공유할지, 프로세스 간에 공유할지 여부를 나타낸다.
- 0인 경우, 동일한 프로세스 내 스레드 간 공유되며, 모든 스레드에서 접근 가능한 메모리 주소에 위치해야 한다.
- 0이 아닌 경우, 여러 프로세스 간 공유되며, 반드시 공유 메모리 영역에 위치해야 한다.
- value : 세마포어의 초기값
✅ 반환값
- 성공시 0을, 실패시 -1을 리턴하고 errno의 값이 설정된다.
✅ sem_post()
#include <semaphore.h>
int sem_post(sem_t *sem);
- 세마포어의 값을 1 증가(= 해제) 시킨다.
- 세마포어의 값이 0보다 커지게 되면, sem_wait() 호출에서 블록 상태에 있던 다른 프로세스나 스레드가 깨어나서 세마포어를 획득할 수 있게 된다.
✅ 반환값
- 성공시 0을, 실패시 -1을 리턴하고 errno의 값이 설정된다.
- 에러 발생 시, 세마포어의 값은 변경되지 않는다.
✅ sem_wait()
#include <semaphore.h>
int sem_wait(sem_t *sem);
- 세마포어의 값을 감소(= 잠금) 시킨다.
- 세마포어의 값이 0보다 큰 경우, 값을 감소시키고 즉시 반환한다.
- 세마포어의 값이 0이면, 호출한 스레드는 다음 중 하나가 될 때까지 블록 된다.
- 다른 스레드가 sem_post()로 값을 증가시킨다.
- 시그널 핸들러가 호출되어 인터럽트 된다.
✅ 반환값
- 성공시 0을, 실패시 -1을 리턴하고 errno의 값이 설정된다.
- 에러 발생 시, 세마포어의 값은 변경되지 않는다.
✅ sem_open()
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
- 새로운 세마포어를 생성하거나, 기존에 존재하는 세마포어를 연다.
- name : 생성 또는 접근하고자 하는 세마포어의 이름
- 이름이 /로 시작하면, 세마포어는 /dev/sem/ 폴더에 생성된다.
- 이름이 /로 시작하지 않으면, 세마포어는 /dev/sem/현재폴더/ 폴더에 생성된다.
- oflag : 세마포어 생성시 플래그. O_CREAT, O_EXCL 두 가지 조합으로 사용 가능하다.
- O_CREAT 설정 시, 두 개의 추가 인자를 전달해야 한다.
- O_CREAT | O_EXCL 조합 시, 세마포어가 이미 존재하는 경우, 에러가 발생하고 생성되지 않는다.
- mode : 새로 생성되는 세마포어에 대해 설정할 권한
- S_IRWXR : 그룹 접근
- S_IRWXO : 타인 접근
- S_IRWXU : 개인 접근
- value : 새로 생성되는 세마포어의 초기 값. unlock 된 세마포어의 수를 의미한다.
- 0보다 큰 양수여야 하고, SEM_VALUE_MAX를 초과할 수 없다.
✅ 반환값
- 성공시 새로운 세마포어의 주소(포인터)를 리턴한다.
- 실패 시, SEM_FAILED를 리턴하며, errno의 값이 설정된다.
sem_open()은 세마포어 디스크립터를 반환하는데, 이는 sem_wait(), sem_trywait(), sem_post() 를 활용하는 데 사용할 수 있다. 디스크립터는 sem_close() 를 호출할 때까지 사용가능하다. 세마포어를 언링크하려면 sem_unlink()를 호출하면 된다.
✅ sem_unlink()
#include <semaphore.h>
int sem_unlink(const char *name);
- name으로 지정된 세마포어를 제거한다.
- name : 제거해야 할 세마포어의 이름.
- 현재 name의 세마포어가 어떤 프로세스에 의해 참조되고 있다면, 해당 세마포어에 영향을 미치지 않는다.
- 모든 참조되고 있는 세마포어가 sem_close() 등에 의해 종료될 때까지 세마포어의 제거는 연기된다.
✅ 반환값
- 성공시 0을, 실패시 -1을 리턴하고 errno의 값이 설정된다.
✅ sem_getvalue()
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
- sem이 가리키는 세마포어의 현재 값을 sval에 저장한다.
- 하나 이상의 프로세스나 스레드가 sem_wait()를 통해 세마포어 잠금을 기다리고 있다면, 두 가지 동작 방식 중 하나를 허용한다.
- 0이 저장된다. → Linux 채택
- 음수 값이 저장되며, 이 음수의 절대값은 현재 블록된 프로세스/스레드 수를 나타낸다.
✅ 반환값
- 성공시 0을, 실패시 -1을 리턴하고 errno의 값이 설정된다.
Condition Variable API
✅ 반환값
- 성공시 0을, 실패시 오류 번호를 리턴한다.
✅ 조건 변수 초기화 방법
1. 정적으로 할당된 조건 변수 초기화 → 오류 검사가 수행되지 않는다.
pthread_cond_t cond= PTHREAD_CONT_INITIALIZER;
pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restric attr);
✅ pthread_cond_init()
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 조건 변수 cond를 초기화하기 위해서 사용된다.
- cond: 변수를 선언하여 첫 번째 인자에 주소값을 넘겨 초기화할 수 있다.
- attr : 조건 변수의 특성을 지정하며, NULL을 지정하면 기본값으로 설정된다.
| ERROR | 설명 |
| EAGAIN | 조건 변수를 새로 초기화 하는 데 필요한 (메모리 외의) 자원이 시스템에 부족하다. |
| ENOMEM | 조건 변수를 초기화 하기에 충분한 메모리가 없다. |
- pthrea_cond_wait() 함수와 pthread_cond_timedwait() 함수는 호출 전에 반드시 뮤텍스를 현재 스레드가 잠금하고 있어야 한다.
※ Spurious Wakeup
POSIX나 모든 OS에서 시그널을 줬을 때 하나만 깨어나는 것이 아닌, 동시에 여러 wait condition이 깨어나는 현상을 뜻한다.
- 조건 변수를 사용할 때는 Spurious Wakeup을 방지하기 위해서 반드시 공유 변수 상태를 따로 확인하는 논리가 필요하다.
✅ pthread_cond_wait()
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- 뮤텍스의 lock을 해제하고 조건 변수 cond가 신호를 받을 때까지 기다린다.
- 뮤텍스를 해제하고 조건 변수에서 대기 상태에 들어가는 과정은 원자적(atomically) 즉, 다른 스레드에 의해 중간에 끼어들 수 없는 상태로 처리된다.
- 이 때, 해당 조건 변수와 뮤텍스 사이에 동적 바인딩이 형성되며, 조건 변수에서 기다리는 스레드가 하나 이상 존재하는 동안 유지된다.
pthread_cond_signal() 또는 pthread_cond_broadcast()에 의해 조건 변수가 깨어나면, 다시 뮤텍스를 자동으로 lock하고 리턴된다.
✅ pthread_cond_timedwait()
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
- pthread_cond_wait() 함수와 동일하지만, 절대 시간(abstime)이 경과할 경우 타임아웃이 발생하여 ETIMEDOUT 에러가 발생한다.
- 타임아웃이 발생해도 조건 신호가 동시에 도착했다면, 이를 소비할 수 있다.
✅ 반환값
- 성공시 0을, 실패시 에러 코드가 리턴된다.
- 실패한 경우, 뮤텍스나 조건 변수의 상태는 변경되지 않는다.
| ERROR | 설명 |
| ETIMEDOUT | 시간초과 검사를 위해 지정된 abstime를 초과해서 시그널이 발생하지 않았을 때. |
| EINTR | (유닉스 POSIX)시그널등에 의해서 인터럽트가 발생했을 때 |
- pthread_cond_signal() 함수와 pthread_cond_broadcast() 함수는 조건 변수에 대해 블로킹 상태에 있는 스레드들을 깨우는 역할을 한다.
- 조건 변수를 기다리는 스레드가 없다면 아무런 일도 일어나지 않는다.
✅ pthread_cond_signal()
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
- pthread_cond_wait() 함수와는 다르게 뮤텍스를 받지 않는다.
- 조건 변수 cond에 시그널을 보내서 cond에 대해 대기 중인 스레드 중 하나 이상을 깨운다.
- 여러개의 스레드가 기다리고 있다면 단 하나만 시작시키지만, 어떤 스레드가 시작될지는 알 수 없다.
✅ pthread_cond_broadcast()
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
- 하나의 스레드만 다시 시작시키는 pthread_cond_signal() 함수와 다르게 조건 변수 cond를 기다리고 있는 모든 스레드를 다시 시작시킨다.
- 여러 스레드가 동시에 깨워졌다면, 서로 뮤텍스를 획득하기 위해 경젱하게 되며, 이는 각 스레드가 pthread_mutex_lock()을 호출한 것처럼 동작한다.
- 공유 변수의 상태가 변경되어 여러 스레드가 동시에 진행할 수 있는 상황에서 사용된다.
- ex) Read-Write Lock
- 쓰기 작업이 끝난 뒤 모든 읽기 대기자를 깨우기 위해 pthread_cond_broadcast()를 사용한다.
✅ 반환값
- 성공시 0을, 실패시 errno 값을 리턴한다.
✅ pthread_cond_destroy()
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
- cond로 지정된 조건 변수를 소멸시킨다.
- 조건 변수에 스레드가 대기중인 상태가 없어야 해제할 때 안전하다.
- 소멸된 객체는 초기화되지 않은 상태가 되며, pthread_cond_init()을 통해 재초기화를 할 수 있다.
'Server > Linux, C' 카테고리의 다른 글
| [C언어] 시그널 (feat. Sigaction) (0) | 2025.09.30 |
|---|---|
| [C언어] 메모리 (feat. 주소록 프로그램) (0) | 2025.09.17 |
| [C언어] 동기화 (feat. Mutex API 사용해 은행 잔고 프로그램 구현하기) (0) | 2025.09.12 |
| [C언어] Thread (4) | 2025.06.25 |
| [C언어] Makefile / GDB (0) | 2025.05.25 |