본문 바로가기

C Language

공유 뮤텍스를 사용하는 이유와 예시

 

모든 철학자 스레드가 동일한 뮤텍스 객체를 공유하여 자원 접근을 동기화하기 위해서 공유뮤텍스를 사용합니다.

1. 출력 동기화 (write_lock)

문제 상황

  • 2명의 철학자가 동시에 "철학자 A가 포크를 들다"와 "철학자 B가 먹기 시작하다"를 출력하려 함
  • 각자 서로 다른 뮤텍스를 사용할 경우:→ 두 출력이 겹쳐 "철학자 A가 포철학자 B가 먹기 시작하다크를 들다" 같이 깨짐
  •  
  •  
    c
    Philosopher A: lock(자신의 write_lock) → 출력 → unlock Philosopher B: lock(자신의 write_lock) → 출력 → unlock

해결 방법

  • 동일 뮤텍스를 공유할 경우:→ 출력이 순차적으로 발생
  •  
    c
    Philosopher A: lock(공유 write_lock) → 출력 → unlock Philosopher B: lock(공유 write_lock)(A의 unlock 대기) → 출력

2. 사망 상태 체크 (dead_lock)

문제 상황

  • 철학자 1이 dead_flag를 1로 설정하는 도중
  • 철학자 2가 동시에 dead_flag를 읽으려 함
  • 서로 다른 뮤텍스 사용 시:→ 철학자 2가 죽었음에도 계속 행동하는 치명적 오류
  •  
    c
    철학자1: lock(자신의 dead_lock) → dead_flag 수정 중(아직 미반영) 철학자2: lock(자신의 dead_lock) → 잘못된 dead_flag(0) 읽음

해결 방법

  • 동일 뮤텍스 사용 시:
  •  
    c
    철학자1: lock(공유 dead_lock) → dead_flag=1 설정 → unlock 철학자2: lock(공유 dead_lock) → 정확한 dead_flag(1) 읽음 → 즉시 행동 중단

3. 식사 횟수 기록 (meal_lock)

문제 상황

  • 철학자 1이 meals_eaten++ 실행 중(현재 값: 3 → 4로 변경 중)
  • 철학자 2가 동시에 meals_eaten 값을 읽으려 함
  • 서로 다른 뮤텍스 사용 시:→ 실제 4여야 하지만 3으로 잘못 인식
  •  
    c
    철학자1: lock(자신의 meal_lock) → 값 증가 중(아직 3) 철학자2: lock(자신의 meal_lock) → 잘못된 값(3) 읽음

해결 방법

  • 동일 뮤텍스 사용 시:
  •  
    c
    철학자1: lock(공유 meal_lock) → 값 4로 업데이트 → unlock 철학자2: lock(공유 meal_lock) → 정확한 값(4) 읽음

기술적 이점 요약

  1. 메모리 효율성: 뮤텍스 인스턴스를 1개만 생성해 모든 스레드가 공유
  2. 동기화 보장: 공유 자원(출력/상태값/식사횟수)에 대한 원자적 접근
  3. 데이터 무결성: 경쟁 조건(race condition) 방지로 프로그램 신뢰성 향상
  4. 확장성: 철학자 수가 늘어나도 동일한 메커니즘 유지 가능

이 구현 방식은 식사하는 철학자 문제에서 교착 상태(deadlock)와 자원 경쟁을 방지하는 전형적인 패턴으로 사용됩니다.

 

그리고 t_philo *philos를 t_program 구조체에 연결하여 철학자들의 상태와 행동을 중앙에서 관리하고 동기화합니다. 이를 통해 프로그램 전체에서 철학자들의 데이터를 효율적으로 접근하고 제어할 수 있습니다. 

1. 중앙 관리의 필요성

philos 배열은 철학자 각각의 정보(스레드, 식사 상태, 포크 등)를 저장합니다. 이를 t_program 구조체에 연결하면, 모든 철학자를 한 곳에서 관리할 수 있습니다. 이는 다음과 같은 상황에서 유용합니다:

  • 전체 철학자의 상태 확인: 예를 들어, 모든 철학자가 식사를 마쳤는지 확인하거나 특정 철학자가 사망했는지 체크할 때, t_program을 통해 모든 데이터를 접근할 수 있습니다.
  • 동기화 및 공유 자원 관리: 각 철학자가 사용하는 포크, 출력 뮤텍스 등을 philos 배열을 통해 쉽게 관리할 수 있습니다.

2. 예시: 철학자의 사망 상태 확인

문제 상황

철학자 중 한 명이 설정된 time_to_die를 초과하여 사망했을 경우, 모든 철학자는 즉시 행동을 멈춰야 합니다.

구현 방식

  • philos[i].dead_flag를 통해 특정 철학자의 사망 여부를 확인합니다.
  • t_program 구조체에 연결된 philos 배열을 순회하여 모든 철학자의 상태를 확인합니다.
 
c
void check_death(t_program *program) { for (int i = 0; i < program->philos[0].num_of_philos; i++) { if (get_current_time() - program->philos[i].last_meal > program->philos[i].time_to_die) { pthread_mutex_lock(&program->dead_lock); program->dead_flag = 1; pthread_mutex_unlock(&program->dead_lock); break; } } }

결과

  • 모든 철학자는 중앙 관리 구조(t_program)를 통해 사망 여부를 즉시 확인하고 행동을 멈춥니다.

3. 예시: 포크 동기화

문제 상황

철학자들이 식사를 하기 위해 양쪽 포크를 사용해야 하지만, 동시에 여러 철학자가 같은 포크를 사용하려고 하면 교착 상태가 발생할 수 있습니다.

구현 방식

  • 각 철학자는 자신의 왼쪽(l_fork)과 오른쪽(r_fork) 포크를 pthread_mutex_t로 관리합니다.
  • t_program 구조체의 philos 배열을 통해 각 철학자의 포크 정보를 공유합니다.
 
c
void pick_up_forks(t_philo *philo) { pthread_mutex_lock(philo->l_fork); pthread_mutex_lock(philo->r_fork); } void put_down_forks(t_philo *philo) { pthread_mutex_unlock(philo->l_fork); pthread_mutex_unlock(philo->r_fork); }

결과

  • 각 철학자는 중앙에서 관리되는 포크 정보를 사용하여 동기화된 방식으로 식사합니다.
  • 교착 상태를 방지하기 위해 포크 사용 규칙을 적용할 수 있습니다.

4. 효율적인 데이터 접근

t_philo *philos 배열을 사용하면 다음과 같은 데이터 접근이 간단해집니다:

  • 특정 철학자의 상태 (philos[i].eating, philos[i].meals_eaten)를 빠르게 확인 가능.
  • 모든 철학자를 순회하며 상태를 업데이트하거나 동작을 제어 가능.

결론

t_philo *philos를 t_program 구조체에 연결하면:

  1. 중앙 관리: 모든 철학자의 데이터를 한 곳에서 통합적으로 관리.
  2. 효율적 동기화: 공유 자원(포크, 출력 등)을 쉽게 동기화.
  3. 확장성: 철학자 수가 증가해도 동일한 구조로 관리 가능.

이는 식사하는 철학자 문제와 같은 동기화 문제에서 필수적인 설계 패턴입니다.

 

네, 맞습니다! int eating과 int meals_eaten은 철학자의 상태와 행동을 나타내는 변수로, 서로 다른 역할을 합니다. 아래에서 두 변수의 차이점과 용도를 상세히 설명하겠습니다.

1. int eating

역할

  • 철학자가 현재 식사 중인지 여부를 나타냅니다.
  • 이 변수는 **상태 플래그(flag)**로 작동하며, 값은 일반적으로 다음과 같습니다:
    • 1: 철학자가 현재 식사 중임을 의미
    • 0: 철학자가 식사 중이 아님을 의미

사용 목적

  • 철학자의 상태를 추적하여 동기화 로직에 활용합니다.
  • 예를 들어, 다른 스레드(감시 스레드 등)가 철학자의 상태를 확인하거나 처리할 때 사용됩니다.

예시

 
c
if (philos[i].eating == 1) { printf("철학자 %d는 현재 식사 중입니다.\n", philos[i].id); } else { printf("철학자 %d는 식사 중이 아닙니다.\n", philos[i].id); }

구체적인 활용

  • 감시 스레드가 철학자의 last_meal 시간을 업데이트하거나, 사망 여부를 확인할 때 사용됩니다:
  •  
    c
    if (philos[i].eating == 0 && get_current_time() - philos[i].last_meal > philos[i].time_to_die) { printf("철학자 %d가 사망했습니다.\n", philos[i].id); *philos[i].dead = 1; }

2. int meals_eaten

역할

  • 철학자가 지금까지 식사를 완료한 횟수를 나타냅니다.
  • 이 변수는 누적 카운터(counter)로 작동하며, 초기값은 0이고 철학자가 식사를 완료할 때마다 증가합니다.

사용 목적

  • 철학자가 특정 횟수만큼 식사를 완료했는지 확인하거나, 모든 철학자가 설정된 식사 횟수를 충족했는지 판단하는 데 사용됩니다.
  • 프로그램 종료 조건을 구현하는 데도 활용됩니다.

예시

 
c
if (philos[i].meals_eaten >= philos[i].num_times_to_eat) { printf("철학자 %d가 모든 식사를 완료했습니다.\n", philos[i].id); }

구체적인 활용

  • 모든 철학자가 설정된 횟수만큼 식사를 했는지 확인하여 프로그램을 종료할 수 있습니다:
  •  
    c
    int all_done = 1; for (int i = 0; i < num_of_philos; i++) { if (philos[i].meals_eaten < philos[i].num_times_to_eat) { all_done = 0; break; } } if (all_done) { printf("모든 철학자가 식사를 완료했습니다. 프로그램을 종료합니다.\n"); return; }

3. 두 변수의 차이점 요약

변수데이터 타입의미주요 용도
eating int 철학자가 현재 식사 중인지 여부 (1 또는 0) 현재 상태 추적 및 동기화 로직에 사용
meals_eaten int 철학자가 지금까지 완료한 식사 횟수 누적된 행동 추적 및 종료 조건 판단
 

4. 예제 코드: 두 변수의 동작 흐름

아래는 eating과 meals_eaten이 함께 사용되는 예제입니다:

 
c
void *philosopher_life(void *arg) { t_philo *philo = (t_philo *)arg; while (*philo->dead == 0) { // 죽지 않은 동안 반복 // 포크 집기 pick_up_forks(philo); // 식사 시작 pthread_mutex_lock(&philo->meal_lock); philo->eating = 1; // 현재 식사 중 philo->last_meal = get_current_time(); printf("철학자 %d가 식사를 시작합니다.\n", philo->id); usleep(philo->time_to_eat * 1000); // 식사 시간 대기 philo->meals_eaten++; // 식사 횟수 증가 philo->eating = 0; // 식사 종료 pthread_mutex_unlock(&philo->meal_lock); // 포크 내려놓기 put_down_forks(philo); // 잠자기 및 생각하기 usleep(philo->time_to_sleep * 1000); } return NULL; }

결론

  • eating: 철학자의 현재 상태를 나타내며, 동기화와 상태 체크에 사용됩니다.
  • meals_eaten: 철학자의 누적 행동을 나타내며, 종료 조건이나 진행 상황 추적에 사용됩니다.

이 둘은 서로 보완적인 역할을 하며, 프로그램의 동작과 논리를 명확히 하기 위해 함께 사용됩니다.

 

'C Language' 카테고리의 다른 글

포크락 수정  (0) 2025.03.15
철학자의 포크 할당 로직 설명  (0) 2025.03.14
mutex 입금문제  (2) 2025.03.13
eat 함수  (0) 2025.03.13
pthread_mutex_init NULL 초기화  (0) 2025.03.13