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언어] 동기화 (feat. Mutex API 사용해 은행 잔고 프로그램 구현하기) 본문

Server/Linux, C

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

YUDENG 2025. 9. 12. 22:07

 

1. Race Condition (경쟁 상태)

 

둘 이상의 실행 주체가 동시에 하나의 공유 자원에 접근하려고 경쟁할 때 발생하는 상태이다. 프로세스마다 실행 속도가 달라서 예상치 못한 결과를 초래할 수 있다. 이를 해결하기 위해서 공유 자원을 한번에 하나의 프로세스만 접근할 수 있도록 제한을 두는 동기화 방식을 취해야 한다.

 

동기화를 위한 대표적인 도구로 뮤텍스(Mutex)세마포어(Semaphore)가 있다.

 

 

2. Critical Section (임계 구역)

한번에 하나의 프로세스만이 진입해야 하는 특정 코드 영역으로 둘 이상의 실행 주체가 동시에 실행될 경우, 경쟁 조건을 발생시킬 수 있는 구간이다.

 

 
프로세스 코드 영역

Critical Section 문제를 해결할 수 있는 방법은 다음 3가지 조건을 만족해야 한다.

  1. 상호 배제 (Mutual Exclusion)
    1. 한 프로세스가 임계 구역에 들어가 있으면 다른 프로세스는 들어갈 수 없다.
  2. 한정 대기 (Bounded waiting)
    1. 상호 배제 때문에 기다리게 되는 프로세스가 무한 대기하지 않아야 한다.
    2. 즉, 특정 프로세스가 임계 구역에 진입하지 못하면 안된다.
  3. 진행의 융통성 (Progress Flexibility, progress)
    1. 임계 구역에 프로세스가 없다면 어떠한 프로세스라도 들어가서 자원을 활용할 수 있다.

 

3. Mutex Lock

하나의 프로세스나 스레드만 자원에 접근할 수 있게 하는 상호 배제 기법으로, Lock을 사용한다.

뮤텍스 락은 임계구역 입장 전 lock을 얻어야 하고, 임계 구역을 나가게 되면 lock을 해제해야 한다.

 

세마포어와는 다르게 하나의 스레드만이 임계 영역에 접근할 수 있다.

 

 

4. Semaphore

  • 세마포어는 음수가 아닌 정수 값을 사용하여 임계 영역에 접근한다.
  • 세마포어 값이 1 이상이면 접근을 허용하고, 0이면 대기한다.
  • 세마포어의 값은 P(임계 영역에 진입)와 V(임계 영역에서 나감) 연산으로 조절된다.

 

세마포어의 연산

  • wait() : 스레드가 임계구역에 들어가기 위해 호출하는 연산, 다른 스레드가 임계구역에 작업중이면 대기한다.
  • signal() : 스레드가 임계구역에 작업을 마치고 세마포어를 해제하는 연산

※ wait()과 signal() 연산은 원자적(atomically)으로 수행되어야 한다. 
-> 세마포어 연산에서 세마포어의 값을 수정하는동안에는 다른 스레드가 선점할 수 없다.

 

wait()과 signal()의 정의

wait(S){
    while(S <= 0) // 이용가능한 세마포어가 없는 경우
        ; // busy wait
    S--;
}

signal(S){
    S++;
}
 

 

이진 세마포어 (Binary Semaphore)

  • 0 또는 1 값만 가질 수 있다.
  • 임계 구역 문제를 해결하는데 사용하며, 자원이 하나이기 때문에 뮤텍스로도 사용할 수 있다.

 

개수 세마포어 (Counting Semaphore)

  • 도메인이 0 이상인 임의의 정수값이다.
  • 여러 개의 자원을 가질 수 있으며, 제한된 작업을 가지고 액세스 작업을 할 때 사용한다.

 

5. Read/Write lock

 

RW lock은 읽기 전용 작업을 위한 동시 접근은 허용하되, 쓰기 작업은 독점적인 접근으로 제한하는 자원 관리 기법이다. 단순 읽기 동작은 내용의 일관성을 해치지 않기 때문에 서로 동시에 같은 내용을 읽어도 동기화 문제가 발생하지 않는다.

 

읽기 접근 (Read Lock)

  • 여러 스레드가 동시에 공유 데이터를 읽을 수 있다.
  • 읽기 작업이 이뤄지는 동안에는 새로운 읽기 잠금이 추가로 획득할 수 있으며, 데이터 일관성 유지에 도움이 된다.

Write Lock (쓰기 접근)

  • 쓰기 작업을 수행하는 스레드는 데이터에 대한 독점적 접근 권한을 요구한다.
  • 쓰기 잠금이 활성되는 동안에는 다른 스레드도 데이터를 읽거나 쓸 수 없다.

 

6. Condition Variable

특정 조건을 만족하기를 기다리는 변수로, 주로 스레드 간 신호 전달을 위해서 사용한다.

condition variable은 waiting과 signaling을 사용하기에 기본적으로 cond_wait()와 cond_signal() 함수를 사용하게 된다. 또한, wait과 signal 내부적으로 unlock()과 lock()이 각각 앞 뒤로 있기 때문에 외부를 lock()과 unlock()으로 감싸야 한다.

하나의 스레드가 waiting 중이면 조건을 만족한 스레드에서 변수를 바꾸고 signaling을 통해 깨우는 방식이다.

 


Mutex API

 

뮤텍스는 fast와 recursive 2가지 종류가 지원된다. lock을 얻은 스레드가 다시 lock을 얻을 수 있도록 할 것인지를 결정하기 위해서 사용한다.

 

✅ 반환값

  • 성공시 0을, 실패시 오류 번호를 리턴한다.

 

✅ 뮤텍스 초기화 방법

 

1. 정적으로 할당된 뮤텍스 초기화

pthread_mutex_t lock = PTHREAD_MUTX_INITIALIZER

 

 

2. 동적으로 할당된 뮤텍스 초기화

pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

 

 

✅ pthread_mutex_init()

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t * mutex, 
         const pthread_mutex_attr *attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 뮤텍스 변수를 초기화하는데 사용하는 함수이다.
  • mutex : 변수를 선언하여 첫 번째 인자에 주소값을 넘겨 초기화할 수 있다.
  • attr : 뮤텍스의 속성을 지정하며, NULL을 지정하면 뮤텍스 기본값으로 설정된다.
    • pthread_mutex_attr : 3가지 상수가 존재한다.
    • PTHREAD_MUTEX_INITIALIZER(fast mutex)
    • PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP(recursive mutex)
    • PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP(mutx 에러 체크용)

 

ERROR 설명
ENOMEM 메모리가 부족하여 mutex를 초기화할 수 없다.
EINVAL attr에 의해 지정된 값이 올바르지 않다.

 

스레드 상태
  • New (새로운 상태): 스레드가 생성되었으나 아직 시작되지 않은 상태.
  • Runnable (실행 가능 상태): 스레드가 실행 중이거나 실행될 준비가 된 상태.
  • 일시 중지 상태들 (Suspended States)
    • Blocked (차단 상태): 스레드가 동기화 락을 기다리는 상태.
    • Waiting (대기 상태): 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태.
    • Timed Waiting (시간 제한 대기 상태): 스레드가 일정 시간 동안 다른 스레드의 작업을 기다리는 상태.
    • Terminated (종료 상태): 스레드의 실행이 완료된 상태.

 

✅ pthread_mutex_lock()

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 뮤텍스의 현재 상태가 unlocked일 경우, 스레드는 lock을 얻고 임계영역게에 진입 후 리턴한다.
  • 뮤텍스가 이미 다른 스레드에 의해 잠겨 있으면 사용 가능해질 때까지 호출 스레드가 블록한다.
  • mutex 유형이 PTHREAD_MUTEX_NORMAL인 경우, 교착 상태 발견이 제공되지 않는다.

pthread_mutex_lock이 어떤 스레드에서 호출되어 lock이 걸렸을 때, 다른 스레드가 임계 구역에 진입하기 위해서 pthread_mutex_lock을 호출했다면, 이 스레드가 pthread_mutex_unlock 할 때까지 기다려야 한다.

 

 

재귀 뮤텍스 (PTHREAD_MUTEX_RECURSIVE)

  • 한 스레드가 같은 뮤텍스로 여러 번 잠글 수 있다.
  • 첫 번째로 잠금할 때 lock 카운트는 1이며, 다시 잠글 때마다 카운트가 증가한다.
  • unlock() 할 때마다 1씩 감소하며, 카운트가 0이 되면 뮤텍스가 해제된다.

 

강건한 뮤텍스 (robust mutex)

  • 락을 가진 스레드가 죽은 경우, 다음에 락을 시도한 스레드는 pthread_mutex_lock()에서 EOWNERDEAD 에러를 받는다.
  • 이 경우 락은 획득되지만, 보호하던 자원의 상태는 일관성이 없는 상태로 간주된다.
  • 상태 복구가 가능하면 pthread_mutex_consistent() 호출로 복구 완료를 알린다.
  • 복구할 수 없는 경우엔 unlock()만 호출하고, 이 뮤텍스는 영구적으로 사용할 수 없는 상태가 된다.

 

✅ pthread_mutex_unlock()

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_lock()을 통해 잠근 뮤텍스를 해제하고, 해당 뮤텍스를 기다리고 있는 다른 스레드가 있다면 그 중 하나를 깨운다.
    • fast 뮤텍스인 경우, 항상 unlock 상태를 되돌려준다.
    • recursive(재귀) 뮤텍스인 경우, 잠겨있는 뮤텍스의 수를 감소시키고 이 수가 0이 된다면 뮤텍스 잠금을 되돌려주게 된다.
  • 대기중이던 스레드가 있는 경우, 스케줄러 정책에 따라 우선순위가 가장 높은 스레드가 뮤텍스의 다음 소유자가 된다.
  • unlock()은 lock을 소유한 스레드만 호출해야 하며, 아닌 스레드가 호출할 경우 에러가 발생할 수 있다.
ERROR 설명
EBUSY mutex가 이미 잠겨 있어서 획득할 수 없다.
EDEADLK 뮤텍스 유형이 PTHREAD_MUTEX_ERRORCHECK이며 현재 스레드가 이미 그 뮤텍스를 소유하고 있다.
EINVAL 프로토콜 속성 값을 PTHREAD_PRIO_PROTECT로 해서 mutex를 생성했으며 호출 스레드의 우선순위가 뮤텍스의 현재 우선순위 상한보다 높다.
 

 

✅ pthread_mutex_destroy()

#include <pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 뮤텍스 객체를 파기한다.
  • 뮤텍스를 제거하기 위해서는 unlock 상태여야 한다.

 

🔷 mutex 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 <errno.h>
#include <pthread.h>
#include <unistd.h>

#define READ_THREAD 3
#define UPDATE_THREAD 2
#define TOTAL_THREAD 5
#define TARGET_BALANCE 10000


pthread_mutex_t mutex;
pthread_cond_t cond;

int balance = 0, flag = 0, turn = 0;

typedef struct read_mutex_t {
	int id;
	float time;
} read_mutex_t;

typedef struct update_mutex_t {
	int id;
	float time;
	int amount;
} update_mutex_t;

void *read_balance(void *arg);
void *update_balance(void *arg);

int main() {

	if(pthread_mutex_init(&mutex, NULL) != 0) {
		printf("[ERRPR] mutex_init: %s\n", strerror(errno));
		return -1;
	}

	if(pthread_cond_init(&cond, NULL) != 0) {
		printf("ERROR] cond_init: %s\n", strerror(errno));
		return -1;
	}

	read_mutex_t read_info[] = {{0, 1.0f}, {1, 0.5f}, {2, 0.2f}};
	update_mutex_t update_info[] = {{3, 0.1f, 1000}, {4, 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_info[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;
                }

	}


	for(i = 0; i < READ_THREAD + UPDATE_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_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);

	return 0;
}

void *read_balance(void *arg) {

	if(arg == NULL) {
		printf("[ERROR] read_balance: arg is NULL\n");
		pthread_exit((void *)(intptr_t) -1);
	}
	
	read_mutex_t *info = (read_mutex_t *)arg;
	
	int id = info->id;
	float time = info->time;


	while(!flag) {

		pthread_mutex_lock(&mutex);

		while(turn != id && !flag) {
			pthread_cond_wait(&cond, &mutex);
		}

		printf("현재 은행 잔고는 %d원입니다.\n", balance);

		turn = (turn + 1) % TOTAL_THREAD;
		pthread_cond_broadcast(&cond);
		pthread_mutex_unlock(&mutex);

		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);
	}

	update_mutex_t *info = (update_mutex_t *)arg;
	
	int id = info->id;
	float time = info->time;
	int amount = info->amount;

	while(!flag) {

        	pthread_mutex_lock(&mutex);
		
		while(turn != id && !flag) {
			pthread_cond_wait(&cond, &mutex);
		}

		if(flag) {
			pthread_mutex_unlock(&mutex);
			break;
		}

		if(balance + amount < 0) {
			printf("은행 잔고가 부족합니다.\n");
		} else {
			balance += amount;
		}

		if(balance >= TARGET_BALANCE) {
			flag = 1;
			printf("현재 은행 잔고는 %d원 입니다.\n", balance);
			pthread_cond_broadcast(&cond);
		}

		turn = (turn + 1) % TOTAL_THREAD;
		pthread_cond_broadcast(&cond);
		pthread_mutex_unlock(&mutex);

		usleep(time * 1000000);

	}

	pthread_exit((void *)(intptr_t) 0);
}

 

 

 

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

Rwlock API ✅ 읽기-쓰기 락 초기화 방법 1. 정적으로 할당된 읽기-쓰기 락 초기화 → 오류 검사가 수행되지 않는다.pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 2. 동적으로 할당된 읽기-쓰기 락 초기화p

uj0791.tistory.com

 

728x90