42/42cursus

[philosophers] 1. 철학자와 멀티스레딩

jaemjung 2022. 2. 18. 21:50

일단 본 과제에서 만들어야 하는 것은 일종의 시뮬레이션이다. 

인자로 들어온 숫자만큼의 철학자들이 식사를 하고, 잠을 자고, 생각을 하는 것의 시뮬레이션인데, 문제는 각 철학자들이 각각 병렬적으로 이 행동을 동시에 실행해야 한다는 것이었다.

 

참으로 친절하게도 문제에서는 스레드를 활용하라는 힌트를 주고 필요한 함수들까지 제시해주고 있었다.

 

그렇다면 스레드란 과연 무엇인가?

스레드는 하나의 프로세스 내에서 실행되고 있는 작업의 흐름을 의미한다.

 

내가 지금까지 42에서 과제를 해오며 만든 프로그램 및 함수들은 모두 하나의 스레드를 가지고 있었다. 즉 하나의 흐름을 타고 조건에 따라 분기를 나눠가며 작업을 수행하는 프로그램들이었던 것이다.

 

그러나 이 철학자 과제에서 철학자들이 개별적으로 행동하게 만들기 위해서는 여러 개의 작업 흐름, 즉 여러 개의 스레드가 필요하다.

 

그런데 지금까지 알고 있었던 바로는 하나의 프로세서는 한 번에 하나의 작업만 처리할 수 있는데, 어떻게 동시에 여러 작업을 병렬적으로 처리할 수 있단 말인가?

 

그 비밀은 바로 컨텍스트 스위칭에 있었다.

 

출처: 위키피디아https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)

멀티스레딩을 사용한다고 하더라도, 하나의 프로세서가 한 번에 한 스레드의 작업밖에 처리할 수 없는 것은 맞다.

하지만 아주 빠르게, 여러 스레드의 작업을 번갈아 가며 처리한다면?

마치 여러 스레드가 동시에 처리되는 것 처럼 보일 것이다.

 

다행히도 오늘날의 프로세서는 매우 매우 훌륭한 성능을 가지고 있으므로, 여러 스레드의 작업을 빠르게 번갈아 가며 처리하여 마치 동시에 여러 스레드가 병렬적으로 처리되는 것처럼 동작할 수 있게 되는데, 이러한 방식으로 멀티스레딩이 가능하게 되는 것이다.

이 과정 중 프로세서가 하나의 스레드의 작업을 잠시 멈추고 다른 스레드의 작업을 수행하기 위해 상태를 변경하는 것을 컨텍스트 스위칭이라고 한다.

 

이렇게 하나의 프로세스 내에서 갈라져 나온 스레드들은 모두 메모리를 공유하므로, 여러 개의 스레드가 같은 변수에 접근 할 수 있게 된다.

 

C언어에서 스레드는 pthread_create라는 함수를 이용하여 생성할 수 있다.

 

int	pthread_create(pthreat_t *thread, 
			const pthreat_attr_t *attr, 
			void *(*start_routine)(void *), 
			void *arg);

 

첫 번째 인자로는 생성될 스레드의 id가 저장될 변수의 주소값을 넣어주면 된다.

두 번째 인자는 스레드의 attribute를 지정해 줄 수 있는 인자인데, NULL을 넣어주면 기본으로 지정된다.

세 번째 인자는 생성될 스레드가 실행하게 될 루틴 함수이다. 루틴 함수는 void 포인터를 인자로 받고 void 포인터를 리턴해야 한다.

네 번째 인자는 세 번째 인자의 루틴 함수에 들어가는 인자로, 역시 void 포인터를 넣어주어야 한다.

스레드 생성에 성공 시 0, 실패 시 상황에 따른 에러코드가 리턴된다.

 

보통 구조체를 하나 생성한 후 거기에 필요한 변수들을 모두 담아 루틴 함수에 void 포인터로 넘겨주고, 루틴 함수에서 적절히 형변환을 하여 루틴함수 내부에서 원하는대로 사용하게 된다.

 

이렇게 pthread_create 함수를 사용하여 생성된 스레드는 생성된 즉시 루틴 함수를 실행하여 병렬로 작업을 시작하게 되는데, 상황에 따라 스레드의 흐름을 제어해줘야 할 수도 있다.

예를들어 특정 스레드를 실행한 후 그 리턴값을 받아와 그 다음의 작업을 처리해야 하는 경우가 있을 수 있다. 이런 경우에는 pthread_join 함수를 사용할 수 있다.

 

int pthread_join(pthread_t thread, void **value_ptr);

 

스레드를 생성 한 후 join함수를 실행하면 해당 스레드의 리턴을 메인스레드에서 기다리게 된다. 스레드의 리턴 값은 두 번째 인자로 들어간 void 포인터에 저장된다. 성공시 0, 실패시 상황에 따른 에러 코드가 리턴된다. 

 

한편 이렇게 생성된 스레드에서 메모리 자원의 관리(동적 할당된 메모리의 free 등...)를 어떻게 해야 하나 머리가 아파오는데, 다행히 pthread create 실행 후 join을 해주면 해당 스레드 종료 시 자동으로 스레드에서 이용한 자원을 반환한다.

아니 그러면 join하지 않고 별도로 계속 돌아가게 놔두고 싶은 애들은...?

 

그런 경우에는 pthread_detach 함수로 스레드 종료 시 모든 자원을 반환할 것을 명시해주면 된다고 한다.

 

int pthread_detach(pthread_t thread);

 

 pthread_create를 호출한 후 detach를 해주면 해당 스레드 종료 시 모든 자원은 자동으로 반환된다. 역시 성공 시 0, 실패시 에러코드가 반환된다. 


이러한 내용을 알게 된 후 본 과제에서 요구하는 철학자 시뮬레이션을 대충 다음과 같은 방식으로 구현하면 되겠다는 생각이 들었다.

1) 일단 들어오는 인자들의 유효성 체크
2) 각 철학자의 정보(철학자의 번호, 스레드 id, 왼쪽 오른쪽 포크 등등..)가 담긴 구조체 배열 생성 및 초기화
3) 각 스레드 생성. 생성된 스레드에서 각 철학자는 포크를 들고 -> 인자로 들어온 시간만큼 먹고 -> 인자로 들어온 시간만큼 자고-> 다음 포크를 집을 수 있을 때까지 생각. 철학자의 상태가 변할 때 마다 로그를 출력.
4) 한편 메인 스레드에서는 철학자들의 상태를 체크. 철학자들이 마지막으로 먹은 시간과 현재 시간의 차를 구하여 이 시간이 죽어야 하는 시간보다 크다면 죽었다는 메시지를 출력하고 시뮬레이션 종료.

 

이런식으로 프로그램의 설계를 대충 하고 시뮬레이션을 구현해보았다. 결과는 대..참사...라고 해야할까? 절대로 죽지 않는 철학자들이 탄생하였다... 나는 내가 처음부터 완벽한 시뮬레이션을 구현한 줄 알았지만... 그럴리가? 게다가 프린트되는 로그가 간헐적으로 섞여서 나오고 있는 대참사가...

 

일단 출력되는 로그들을 자세히 살펴보니, 철학자들이 "응 포크 복사하면 그만이야~" 라는 식으로 제각기 포크를 멋대로 복사해서 집고서는 식사를 계속 하고 있었다.

분명히 포크는 철학자의 숫자와 동일하므로, 5명이 식사를 한다 치면 한 번에 최대 식사를 할 수 있는 철학자는 2명일텐데, 4명의 철학자가 동시에 식사를 하고 있었던 것이다.

 

내가 처음 철학자들의 식사를 구현 한 것은 bool 변수로 포크를 선언하여, 각 철학자가 bool 변수가 true일때까지 대기한 후 식사를 시작하도록 하는 방법이었는데,

이 철학자들이 동시 다발적으로 변수에 접근을 하다보니 -> 한 철학자가 포크를 집고 해당 변수를 false로 갱신하려는 찰나에 / true 상태였던 포크에 다른 철학자가 접근하여 두 철학자 모두가 포크를 잡은 상태가 되어버리는 것이었다.

 

즉, 스레드가 동시에 실행되다 보니, 포크라는 bool 변수에 접근하는 순서를 정해 줄 수 없다는 것이 문제였다... 프린트가 섞여서 나오는 것도 이러한 문제인 듯 했다. 저마다 출력결과를 뿜어내고 있으니, 터미널에서는 로그가 뒤엉켜서 나오고 있었던 것이다.

 

어찌하나 고민하던 찰나, 천재적인 선조 개발자분들은 이미 이러한 문제에 대한 해결책을 가지고 있었으니 그것은 바로... MUTEX라는 것이었다...