) Philosophers - Implement 1

Philosophers

원탁에 앉은 철학자들이 양쪽에 놓인 2개의 포크를 가지고 식사를 해야하는 상황에서, 옆자리의 철학자와 동시에 식사하지 않고 번갈아 가면서 식사해야 하며 이때 발생할 수 있는 문제들을 다루는 과제
= 복수의 프로세스 및 스레드가 동시 동작할 때 발생하는 문제를 다룬다.

- 멀티 스레드 동시성 문제

변수와 같은 자원을 여러 스레드가 공유하는 상황에서 동시에 같은 데이터에 접근 하는 경우 발생하는 문제.
읽기 모드로 접근하는 경우 값이 변질될 우려가 없으나, 쓰기 모드로 여러 스레드가 동시에 접근하는 경우 접근 순서에 따라서 스레드가 읽어오게 되는 값이 달라질 소지가 생긴다. 또한 동시에 접근을 한다면 어떤 스레드가 값을 업데이트 하는 도중 다른 스레드가 그 값에 접근하더라도 변화된 값을 읽지 못하게 되는 상황이 발생할 수 있다. 이처럼 여러 개의 스레드가 공유된 자원에 접근 할 때 데이터의 신뢰성을 보장받을 수 없는 경우를 스레드 동시성(Concurrency) 문제라고 한다.

- 교착 상태(Deadlock)

두개 이상의 프로세스가 서로의 작업이 끝나기만을 기다리고 있어 둘다 영원히 끝나지 않는 상황을 가리킨다.
= 모든 철학자가 각자 오른쪽에 놓인 포크를 집고 아무도 놓지 않아 아무도 식사를 하지 못하는 상황

  • 교착상태의 조건
    (아래 4가지 조건이 모두 만족하면 데드락이 발생할 가능성이 있으며, 하나라도 만족하지 않으면 절대 발생하지 않는다.)
    - 상호 배제(Mutual exclusion)
    한 리소스는 한번에 한 프로세스만이 사용 할 수 있음
    - 점유와 대기(Hold and wait)
    어떤 프로세스가 하나 이상의 리소스를 점유하고 있으면서 다른 프로세스가 가지고 있는 리소스를 기다리고 있음
    - 비선점(No preemption)
    프로세스가 테스크를 마친 후 리소스를 자발적으로 반환할 때 까지 기다림 (강제로 빼앗지 않는다)
    - 환형 대기(Circular wait)
    Hold and wait 관계의 프로세스들이 서로를 기다림

- 기아 상태(Starvation)

특정 프로세스의 우선순위가 낮아서 원하는 자원을 계속 할당 받지 못하는 상태를 말한다.

  • 해결방법
    - 프로세스 우선순위 수시 변경을 통해 각 프로세스 높은 우선순위를 가지도록 기회 부여
    - 오래 기다린 프로세스의 우선순위 높이기
    - 우선순위가 아닌 요청 순서대로 처리하는 요청큐 사용

0. 프로세스와 스레드

프로세스 : 운영체제로부터 자원을 할당받은 작업의 단위
스레드 : 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위

프로그램 -> 프로세스 -> 스레드

프로그램 -> 프로세스

프로그램 : 파일이 저장 장치에 저장되어 있지만 메모리에는 올라가 있지 않은 정적인 상태를 말한다.

  • 올라가 있지 않은 : 아직 운영체제가 프로그램에게 독립적인 메모리 공간을 할당해주지 않았다는 뜻이다. 모든 프로그램은 운영체제가 실행되기 위한 메모리 공간을 할당해 줘야 실행될 수 있다.
  • 정적인 상태: 정적(靜的)이라는 단어 그대로, 움직이지 않는 상태라는 뜻이다. 한 마디로 아직 실행되지 않고 가만히 있다는 뜻이다.

즉 프로그램이란 아직 실행되지 않은 파일 그 자제를 말한다. 윈도우의 .exe 파일이나, macOS의 .dmg 파일 등 사용자가 눌러서 실행하기 전의 파일을 말한다. 쉽게 말해 그냥 코드 덩어리다.

프로그램을 실행하는 순간 해당 파일은 컴퓨터 메모리에 올라가게 되고, 이 상태를 동적인 상태라고 하며 이 상태의 프로그램을 프로세스라고 한다. 프로세스는 즉 실행되고 있는 컴퓨터 프로그램이라고 정의할 수 있으며 스케줄링 단계에서의 작업과 같은 단어라고 볼 수 있다. (프로세스 = 작업 중인 프로그램)

프로그램은 코드 덩어리 파일이고 그 프로그램을 실행한 게 프로세스.

프로세스 -> 스레드

과거에는 프로그램을 실행할 때 시작부터 끝까지 프로세스 하나만을 사용해서 진행했다. 하지만 시간이 흐를수록 프로그램이 복잡해지고 프로세스 하나만으로 프로그램을 실행하기엔 벅차게 되었다. 실제로 현재 프로그램 하나가 단순이 한 가지 작업만을 하는 경우는 드물다.

이를 어떻게 보완할 수 있을까? 쉽게 떠오르는 방법은 '한 프로그램을 처리하기 위한 프로세스를 여러 개 만들기'지만 이는 불가능하다. 왜냐하면 운영체제는 안전성을 위해서 프로세스마다 자신에게 할당된 메모리 내의 정보에만 접근할 수 있도록 제약을 두고 있고, 이를 벗어나는 정보에 접근하면 오류가 발생하기 때문이다.

이는 즉 프로세스와는 다른 더 작은 실행 단위의 개념이 필요하게 되었고, 이 개념이 바로 스레드이다. 스레드는 프로세스 특성의 한계를 해결하기 위해 만들어진 개념이기 때문에 스레드의 특성은 쉽게 유추할 수 있다. 스레드는 프로세스와 달리 스레드 간 메모리를 공유하며 작동한다. 스레드끼리 프로세스의 자원을 공유하면서 프로세스 실행 흐름의 일부가 되는 것이다. 프로그램이 코드 덩어리라면, 스레드는 코드 내에 선언된 함수들이 되고 따라서 main함수 또한 일종의 스레드라고 볼 수 있다.

스레드는 프로세스의 코드에 정의된 절차에 따라 실행되는 특정한 수행 경로.

프로세스와 스레드의 작동 원리

위에서 프로세스가 메모리에 올라갈 때 운영체제로부터 시스템 자원을 할당 받는다고 언급했다. 이 때 운영체제는 프로세스마다 각각 독립된 메모리 영역을 Code/Data/Stack/Heap의 형식으로 할당해 준다. 각각 독립된 메모리 영역을 할당해 주기 때문에 프로세스는 다른 프로세스의 변수나 자료에 접근할 수 없다.

이와 달리 스레드는 메모리를 서로 공유할 수 있다고 언급했다. 즉 스레드는 프로세스가 할당받은 메모리 영역 내에서 Stack 형식으로 할당된 메모리 영역은 따로 할당 받고, 나머지 Code/Data/Heap 형식으로 할당된 메모리 영역을 공유한다. 따라서 각각의 스레드는 별도의 스택을 가지고 있지만 힙 메모리는 서로 읽고 쓸 수 있다.

이 내용을 바탕으로 프로세스와 스레드 간의 중요한 차이가 하나 더 있다. 만약 프로세스를 실행하다가 오류가 발생해 프로세스가 강제로 종료되면, 다른 프로세스에게 어떤 영향이 있을까? 공유하고 있는 파일을 손상시키는 경우가 아니라면 아무런 영향을 주지 않는다.

하지만 스레드의 경우는 다르다. 스레드는 Code/Data/Heap 메모리 영역의 내용을 공유하기 때문에 어떤 스레드 하나에서 오류가 발생한다면, 같은 프로세스 내의 다른 스레드 모두가 강제로 종료된다.

이와 같이 스레드를 코드(프로세스) 내에서의 함수(스레드)에 빗대어 표현해보면 이해하기 훨씬 쉬워진다. 예를 들어 코드 내 어떤 함수 하나에서 Segmentation Fault 등의 오류가 발생하면, 해당 코드는 다른 함수 모두에 대한 작업을 중단하고 프로세스 실행을 끝내버리는 상태와 같다.

1. philo.h

프로세스 구조체

typedef struct s_state
{
	//인자로 받은 
    //철학자 수, 철학자 수명, 식사 시간, 잠자는 시간, 각 철학자가 최소한 밥을 먹어야 하는 횟수 
	int					nb;
	int					time_die;
	int					time_eat;
	int					time_sleep;
	int					must_eat;
    //먹기 시작한 시간 저장 변수
	unsigned long long	start;
    //죽은 철학자 체크 변수
	int					died;
    //스레드 구조체(철학자 구조체)
	t_philo				philos[300];
    //뮤텍스 변수로 설정한 포크
	pthread_mutex_t		forks[300];
    //
	pthread_mutex_t		write;
}	t_state;

스레드의 구조체

typedef struct s_philo
{
	//철학자 번호
	int					id;
    //왼쪽, 오른쪽 포크의 사용가능 여부
	int					l_fk;
	int					r_fk;
	unsigned long long	last_eat;
	int					total_eat;
	pthread_t			thread;
	struct s_state		*state;
}				t_philo;

2. 스레드 함수

#include <pthread.h>

       int pthread_create(pthread_t *restrict thread,
                          const pthread_attr_t *restrict attr,
                          void *(*start_routine)(void *),
                          void *restrict arg);
       //새로운 스레드를 스레드 속성 attr에 따라 생성한다.
       //스레드 속성 객체 attr이 NULL이라면 기본 스레드 속성으로 스레드를 생성한다.
       //스레드가 성공적으로 생성되면 생성된 스레드 ID는 thread에 저장된다.
       //생성된 스레드는 start_routine을 arg 인자를 사용하여 실행한다.
       //start_routine이 반환되면 내부적으로 pthread_exit() 함수가 호출되어 스레드가 종료된다.

	   ! pthread_create() 함수에서 attr을 NULL로 주어 기본적으로 생성된 스레드는 pthread_join() 혹은 pthread_detach()로 종료를 시켜줘야 한다. 
       그렇지 않으면 아무리 스레드가 종료되었다고 해도 자원이 반환되지 않는다. 
       이렇게 남겨진 자원은 메모리릭으로 간주되기 때문에 pthread_join은 쓰레드간의 동기작업과 자원 해제를 위해 필수적이다. 
       생성된 스레드가 종료될 때 알아서 자원을 시스템에게 반환하는 pthread_detach 옵션도 있다.
       
       int pthread_join(pthread_t thread, void **retval);
       //스레드 종료를 대기한다. main 스레드가 추가적으로 생성된 스레드의 종료 상태를 확인하고 자신이 최종적으로 종료하겠다는 내포적인 의미를 갖는다.
       //대기하는 스레드가 종료되면, retval 인자의 값은 pthread_exit() 함수가 전달한 종료 값을 얻게된다.
       
       int pthread_detach(pthread_t thread);
       //기본적으로 스레드는 조인할 수 있다. 만약 스레드의 종료 상태를 main 스레드에서 확인할 필요가 없거나 추가적인 스레드의 뒷정리를 시스템에서 자동적으로 하기를 원할 때는 스레드 detach함수를 통해서 분리한다.
       //성공하면 0리턴, 오류 발생시 에러번호(양수) 리턴

3. 뮤텍스 함수

Mutex : 상호 배제(mutual exclusion)의 약자로 Critical Section을 가지는 쓰레드들의 Running time이 서로 겹치지 않도록 해주는 기법. 1개의 스레드만이 공유 자원에 접근하도록 함.

  • 뮤텍스 Lock, Unlock
    특정 코드 영역의 스레드를 실행할 때 한번에 하나의 스레드만 실행 가능하도록 Lock을 건다. 그 영역에 접근하려던 다른 스레드들은 Unlock 상태가 될때까지 기다렸다가 Unlock이 되면 해당 공유 자원에 접근할 수 있게 된다.
#include <pthread.h>

	int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    뮤텍스 객체를 초기화한다.
     
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    뮤텍스 객체를 잠근다.

	int pthread_mutex_unlock(pthread_mutex_t *mutex);
    뮤텍스 객체의 잠금을 해지한다.
     
	int pthread_mutex_destroy(pthread_mutex_t *mutex);
    뮤텍스 객체를 제거한다.
     
	int pthread_detach(pthread_t thread);
    인자 thread를 커널에서 분리 시킨다. 분리된 스레드는 수행을 종료 시키고, 할당된 자원을 회수한다.

좋은 웹페이지 즐겨찾기