Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

우당탕탕 개발일지

[C언어] 동기화 API (feat. Rwlock API 사용해 은행 잔고 프로그램 구현하기) 본문

Server/Linux, C

[C언어] 동기화 API (feat. Rwlock API 사용해 은행 잔고 프로그램 구현하기)

YUDENG 2025. 9. 12. 22:14

 

 

[C언어] 동기화 (feat. Mutex API 사용해 은행 잔고 프로그램 구현하기)

1. Race Condition (경쟁 상태) 둘 이상의 실행 주체가 동시에 하나의 공유 자원에 접근하려고 경쟁할 때 발생하는 상태이다. 프로세스마다 실행 속도가 달라서 예상치 못한 결과를 초래할 수 있다.

uj0791.tistory.com

 

Rwlock API

 

✅ 읽기-쓰기 락 초기화 방법

 

1. 정적으로 할당된 읽기-쓰기 락 초기화 → 오류 검사가 수행되지 않는다.

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
 
2. 동적으로 할당된 읽기-쓰기 락 초기화
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;
2. 동적으로 할당된 뮤텍스 초기화
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()을 통해 재초기화를 할 수 있다.
728x90