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. 멀티스레드: 하나의 프로세스에서 두 개의 스레드를 생성해 입금과 출금 작업을 동시에 수행
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)
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;
}
끝.