뮤텍스를 활용한 입출금 예제 프로그램 수정

김도현 KimDohyun·2024년 12월 15일
0

7장 threading 강의자료 42~45 페이지 mutex example 소스코드에서 start_thread를 deposit과 withdraw로 나누어 하나의 스레드에서는 입금, 다른 스레드에서는 출금을 각각 5회씩 수행하도록 소스코드를 수정하여 제출하시오.
Makefile과 소스파일을 함께 압축하여 제출하시기 바랍니다.

현재 예시 코드에서는 두개의 스레드가 start_thread를 통해 출금만 5번 수행하는 기능으로 구현되어 있습니다.
이같은 start_thread를 deposit과 withdraw으로 나누어 구현하여 각각 입금(이미 start_thread에 구현된 내용을 입금을 수행하도록 코드와 함수의 이름을 deposit으로 수정)과 출금(이미 start_thread에 구현된 내용을 이용하되 withdraw로 함수명칭만 수정)을 두개의 스레드가 5회씩 수행하도록 소스코드를 수정하여 제출해주시기 바랍니다.

예시 코드

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define NUM_THREADS 5

// Shared variable
int counter = 0;

// Mutex for synchronizing access to counter
pthread_mutex_t mutex;

void* increment(void* arg) {
    for (int i = 0; i < 100; i++) {
        pthread_mutex_lock(&mutex);  // Locking the mutex
        counter++;
        printf("Thread %ld incremented counter to %d\n", (long)arg, counter);
        pthread_mutex_unlock(&mutex);  // Unlocking the mutex
        usleep(100);  // Simulate some work
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int rc;

    // Initialize the mutex
    pthread_mutex_init(&mutex, NULL);

    // Create threads
    for (long t = 0; t < NUM_THREADS; t++) {
        rc = pthread_create(&threads[t], NULL, increment, (void*)t);
        if (rc) {
            printf("Error: Unable to create thread %ld\n", t);
            exit(-1);
        }
    }

    // Wait for threads to complete
    for (int t = 0; t < NUM_THREADS; t++) {
        pthread_join(threads[t], NULL);
    }

    printf("Final counter value: %d\n", counter);

    // Destroy the mutex
    pthread_mutex_destroy(&mutex);
    pthread_exit(NULL);
}

일곱 번째 과제는 start_thread 함수를 입금과 출금을 각각 수행하는 두 개의 함수로 나누어 구현하는 것이다.
입금과 출금 구현은 멀티스레드를 생성하고, 각 함수에서 임계 구역을 보호하기 위해 뮤텍스(Mutex)를 사용하면 된다.

개요

이번 과제에서는 데드락(Deadlock)이 발생하지 않도록 설계되어 있기 때문에 별도로 데드락 방지 기법을 사용하지 않는다.

  1. 뮤텍스가 하나만 사용됨
  • 과제에서 계좌 잔고를 보호하기 위해 하나의 뮤텍스만 사용
  • 데드락은 여러 뮤텍스를 사용하는 경우 스레드가 서로 다른 뮤텍스를 잠그고 대기할 때 발생
    → 따라서 하나의 뮤텍스를 사용하면 데드락 발생 X
  1. 잠금 순서가 일정함
  • 각 스레드(deposit, withdraw)는 동일한 방식으로 뮤텍스를 잠그고 작업을 수행한 뒤 즉시 해제
  • 데드락은 잘못된 잠금 순서로 인해 발생하는 경우가 많지만 이번 과제에서는 잠금-해제 순서가 명확하게 설정됨

이번 과제에 필요한 운영체제 개념은 다음과 같다.

1. 멀티스레드: 하나의 프로세스에서 두 개의 스레드를 생성해 입금과 출금 작업을 동시에 수행
2. 뮤텍스: 입금과 출금 작업 중에 공유 자원(잔고)에 대한 동시 접근을 막기 위해 사용
3. 임계 구역: 잔고를 변경하는 코드 영역으로 뮤텍스를 통해 보호
4. 데이터 경쟁: 스레드가 동시에 잔고를 수정하면 발생할 수 있는 문제로 뮤텍스와 동기화를 통해 방지

멀티스레드

하나의 프로세스 내에서 여러 실행 단위(스레드)를 생성하고 관리하는 것

스레드들은 같은 메모리 공간을 공유하며, 멀티스레드 프로세스를 통해 병렬 실행이 가능하다.

예를 들어, 한 스레드가 파일을 읽는 동안(대기 상태) 다른 스레드는 계산 작업을 계속 수행할 수 있어 프로그램이 멈추지 않고 반응할 수 있다.

또한, 스레드 간 작업 전환(컨텍스트 스위칭)이 프로세스 전환보다 훨씬 빠르기 때문에 시스템 자원을 절약할 수 있다.

뮤텍스

임계 구역에서의 상호 배제(Mutual Exclusion)를 보장하는 도구

한 스레드가 자원을 사용하는 동안 다른 스레드가 기다리도록 제어함으로써 데이터 충돌을 방지한다.

예를 들어, 여러 스레드가 하나의 변수에 동시에 값을 변경하려고 할 때 뮤텍스를 사용하면 한 번에 하나의 스레드만 이 작업을 수행할 수 있도록 보호할 수 있다.

뮤텍스는 pthread_mutex_lock으로 잠그고 작업이 끝난 후 pthread_mutex_unlock으로 잠금을 해제한다.

예시

pthread_mutex_lock(&mutex);
// 임계 구역에서의 작업
pthread_mutex_unlock(&mutex);

임계 구역

스레드 간 데이터 충돌이 발생할 가능성이 있는 코드 영역

데이터의 무결성을 보장하려면 임계 구역에서 한 번에 하나의 스레드만 작업할 수 있도록 동기화가 필요하다.

예를 들어, 두 스레드가 동시에 같은 변수에 접근해 값을 변경하면 최종 결과가 예상과 다를 수 있다.

이러한 문제를 방지하기 위해 임계 구역에서는 한 번에 하나의 스레드만 작업할 수 있어야 하며, 이를 뮤텍스와 같은 동기화 도구로 보호해야 한다.

스레드 동기화

프로그램의 올바른 동작을 위해 스레드 간 실행 순서를 조율하는 기술

스레드 동기화를 통해 여러 스레드가 공유 자원에 동시에 접근하지 못하게 하거나 특정 스레드가 작업을 완료한 후 다른 스레드가 작업을 시작하도록 조율할 수 있다.

예를 들어, 하나의 스레드가 데이터를 준비한 후에만 다른 스레드가 이를 처리하도록 동기화를 설정할 수 있다.

동기화가 없으면 스레드가 동시에 자원에 접근하거나 잘못된 순서로 작업을 처리해 데이터 충돌과 같은 문제가 발생할 수 있으므로 프로그램이 안정적으로 작동하려면 스레드 동기화가 필수적이다.

데이터 경쟁

여러 스레드가 동시에 같은 자원에 접근할 때 예상치 못한 결과가 발생하는 상황

데이터 경쟁은 스레드 간 실행 순서와 타이밍이 예측 불가능하기 때문에 발생한다.

예를 들어, 두 스레드가 동시에 하나의 변수를 읽고 수정하려 할 때 실행 순서에 따라 결과가 달라질 수 있다.

이러한 문제는 디버깅이 매우 어렵고 프로그램의 데이터 무결성을 해칠 수 있기 때문에 데이터 경쟁을 방지하려면 반드시 뮤텍스와 같은 동기화 도구를 사용해 스레드 간의 자원 접근을 제어해야 한다.


구현

스레드 생성 함수 분리

// 기존
pthread_create(&thread1, NULL, start_thread, (void *)&owner1);
pthread_create(&thread2, NULL, start_thread, (void *)&owner2);

// 변경
pthread_create(&thread1, NULL, deposit, (void *)&owner1);
pthread_create(&thread2, NULL, withdraw, (void *)&owner2);

두 개의 스레드를 생성하여 입금과 출금 작업을 병렬로 수행하기 위해 멀티스레드를 사용하고, 두 스레드가 같은 자원(계좌 잔고)을 공유하므로 데이터 충돌을 방지하고 작업의 충돌을 막기 위해서 스레드 동기화를 적용했다.

start_thread 함수 → deposit 및 withdraw로 분리

// 기존 (start_thread)
void* start_thread(void* owner) {
    int i = 0;
    int amount_to_withdraw = 0;

    srand(time(NULL));

    for (i = 0; i < 5; i++) {
        sleep(rand() % MAX_SLEEP);
        amount_to_withdraw = rand() % MAX_AMOUNT_TO_WITHDRAW + 1;

        withdraw_func(((struct thread_info*)owner)->saving, ((struct thread_info*)owner)->name, amount_to_withdraw);
    }

    return ((struct thread_info*)owner)->name;
}

// 변경(deposit, withdraw)
void* deposit(void* owner) {
    int i = 0;
    int amount_to_deposit = 0;

    srand(time(NULL));

    for (i = 0; i < 5; i++) {
        sleep(rand() % MAX_SLEEP);
        amount_to_deposit = rand() % MAX_AMOUNT_TO_DEPOSIT + 1;

        deposit_func(((struct thread_info*)owner)->saving, ((struct thread_info*)owner)->name, amount_to_deposit);
    }

    return ((struct thread_info*)owner)->name;
}

void* withdraw(void* owner) {
    int i = 0;
    int amount_to_withdraw = 0;

    srand(time(NULL));

    for (i = 0; i < 5; i++) {
        sleep(rand() % MAX_SLEEP);
        amount_to_withdraw = rand() % MAX_AMOUNT_TO_WITHDRAW + 1;

        withdraw_func(((struct thread_info*)owner)->saving, ((struct thread_info*)owner)->name, amount_to_withdraw);
    }

    return ((struct thread_info*)owner)->name;
}

입금과 출금 작업을 각각 독립적으로 처리하기 위해 기존의 start_thread 함수를 입금 함수(deposit)와 출금 함수(withdraw)로 분리하여 구현했다.

입금과 출금 작업 모두 잔고(balance)라는 공유 자원에 접근하므로, 데이터 충돌을 방지하고 올바른 동작을 보장하기 위해 스레드 동기화를 사용했다.

입금 및 출금 기능 호출 변경

// 기존
withdraw_func(((struct thread_info*)owner)->saving, ((struct thread_info*)owner)->name, amount_to_withdraw);

// 변경
deposit_func(((struct thread_info*)owner)->saving, ((struct thread_info*)owner)->name, amount_to_deposit);
// (in deposit function)

withdraw_func(((struct thread_info*)owner)->saving, ((struct thread_info*)owner)->name, amount_to_withdraw);
// (in withdraw function)

입금과 출금 작업을 구분하여 수행하기 위해 기존 코드에서 출금 작업만 호출하던 withdraw_func 대신 입금 함수(deposit_func)와 출금 함수(withdraw_func)를 각각 호출하도록 변경했다.

입금과 출금 작업이 모두 잔고(balance)를 수정하므로, 데이터 충돌을 방지하고 작업의 일관성을 유지하기 위해 임계 구역과 이를 보호하는 뮤텍스를 활용했다.


최종 코드

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

#define MAX_SLEEP 5
#define INIT_BALANCE 1000
#define MAX_SIZE 32
#define MAX_AMOUNT_TO_WITHDRAW 100
#define MAX_AMOUNT_TO_DEPOSIT 100

struct account {
	pthread_mutex_t* mutex;
	int balance;
};

struct thread_info {
	struct account* saving;
	char name[MAX_SIZE];
};

void* deposit(void*);
void* withdraw(void*);
int withdraw_func (struct account*, char*, int);
int deposit_func (struct account*, char*, int);
int disburse_money(int);

int main(void) {
	pthread_t thread1, thread2;
	pthread_mutex_t the_mutex;

	pthread_mutex_init(&the_mutex, NULL);

	struct account saving;
	saving.balance = INIT_BALANCE;
	saving.mutex = &the_mutex;

	struct thread_info owner1;
	struct thread_info owner2;
	
	owner1.saving = &saving;
	strcpy(owner1.name, "Dohyun");
	owner2.saving = &saving;
	strcpy(owner2.name, "Minseok");

	pthread_create (&thread1, NULL, deposit, (void *)&owner1);
	pthread_create (&thread2, NULL, withdraw, (void *)&owner2);

	pthread_join (thread1, NULL);
	pthread_join (thread2, NULL);

	return 0;
}

void* deposit(void* owner) {
	int i = 0;
	int amount_to_deposit=0;

	srand(time(NULL));

	for (i=0; i<5; i++) {
		sleep(rand()%MAX_SLEEP);
		amount_to_deposit = rand()%MAX_AMOUNT_TO_DEPOSIT + 1;

		deposit_func(((struct thread_info*)owner)->saving, ((struct thread_info*)owner)->name, amount_to_deposit);
	}
	
	return ((struct thread_info*)owner)->name;
}

void* withdraw (void* owner) {
	int i = 0;
	int amount_to_withdraw = 0;

	srand(time(NULL));

	for (i=0; i<5; i++) {
		sleep(rand()%MAX_SLEEP);
		amount_to_withdraw = rand()%MAX_AMOUNT_TO_WITHDRAW + 1;

		withdraw_func(((struct thread_info*)owner)->saving, ((struct thread_info*)owner)->name, amount_to_withdraw);
	}

	return ((struct thread_info*)owner)->name;
}

int withdraw_func (struct account *account, char* name, int amount) {
	pthread_mutex_lock (account->mutex);
	const int balance = account->balance;
	if (balance < amount) {
		pthread_mutex_unlock (account->mutex);
		return -1;
	}
	account->balance = balance - amount;
	printf("%s withdraw %d.\n", name, amount);
	pthread_mutex_unlock (account->mutex);
	
	disburse_money (account->balance);

	return 0;
}

int deposit_func(struct account *account, char* name, int amount) {
	pthread_mutex_lock(account->mutex);
	account->balance += amount;
	printf("%s deposit %d.\n", name, amount);
	pthread_mutex_unlock(account->mutex);

	disburse_money(account->balance);

	return 0;
}

int disburse_money(int balance) {
	printf("Current balance %d.\n", balance);
	return 0;
}

끝.

0개의 댓글