Thread - Philosophers

nhwang·2022년 8월 12일
0

DeadLock
원형 테이블에서 철학자가 각자 1개씩의 포크를 집어들고 있게 된다면,
포크라는 자원은 1/2의 철학자를 먹일 수 있음에도 불구하고
먹지 못하게된다.

이러한 교착상태를 DeadLock이라고 한다.

데드락을 제거하는 방법에는 아래 3가지 방법이 일반적이라 한다.
예방 / 회피(방관) / 수정(내버려 둔 다음 문제 발생 시 수정)
출처 : 주니온TV

예방의 경우에는 계속 조건을 확인하기에 사실 리소스를 많이 먹고, 완전한 방지를 하기엔 굉장히 어렵다.

Monitor Solution
데드락을 방지하기 위해 실제로 포크를 2개 집을 수 있는 철학자만 포크를 집게하면
교착상태의 낭비를 예방할 수 있다.
이를 Monitor solution이라 한다.

*과제에서 적용 가능한지는 좀 더 봐야할듯?


Data Race(데이터 경합)

여러 스레드에서 하나의 데이터에 동시에 접근하게 될 때,
이 데이터가 그 때문에 뒤죽박죽으로 영향을 받게 되는 상황.

Mutex(mutual exclusion)

이런 데이터 경합을 방지하기 위해 하나가 접근해있을 때, 다른 곳에서의 접근을 막는 것을 뮤텍스라 한다.

*Destroy해도 Lock된건 Unlock되지 않음에 주의한다. (삭제 전 언락을 해야할 것)


관련 함수 및 실험

참고 블로그

pthread_create
POSIX thread의 약자로 유닉스 계열 POSIX 시스템에서 스레드를 편하게 만들 수 있게 도와주는 API이다.

int pthread_create(phtread_t *thread, const phtread_attr_t *attr, void *(*start_routine)(void *), void *arg);

1 번째 매개변수인 pthread_t *thread는 쓰레드 식별자로서 생성된 스레드를 담을 쓰레드의 주소 정도로 생각

2 번째 매개변수인 const phread_attr_t *attr은 쓰레드 특성을 지정하기 위해 이용하는데,
메뉴얼을 살펴보니 그룹으로 지정하는 방법도 있는 것 같음.
대개 NULL 처리 해버리니 크게 신경쓰지 않아도 될 듯하다.

3 번째 매개변수인 void (start_routine)(void *)은 thread가 실행되었을 때 시작할 함수이다.

4 번째 매개변수인 void *arg는 세 번째 매개변수인 함수의 인자로 들어갈 인자이다.

pthread_create를 한번 실행시키면 하나의 스레드가 추가로 생성되기 때문에 총 두 개의 쓰레드
(main쓰레드, 새로 생성한 쓰레드)가 동작하게 된다.


실험1 :
참조 내용들을 살펴보니 usleep이 실행되면 다른 스레드가 실행된다는 내용이 있었다
이를 확인하기 위한 실험을 진행.

그렇다면 2개의 스레드가 있고, 서로 다른 시간으로 usleep을 주고받으면
작업 속도에 상관없이 2개의 스레드가 번갈아 가며 일을 해야할 것이라는 생각으로 아래 코드 실행
작업 시간 : main thread > second thread

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

void *t_function(void *param)
{
    printf("쓰레드2 함수 실행\n");
    for (int i = 1; i <= 5; i++)
    {
        usleep(500 * 500);
        printf("쓰레드2 함수 실행 중..(%d / 5)\n",i);
    }
        printf("쓰레드2 함수 종료\n");
        return (void *)2147483647;  // result에 담길 값.
}

void    p_function(void)
{
    for (int i = 0; i <= 10; i++)
    {
        printf("main 스레드 실행 : %d\n ",i);
        usleep(1000*1000);
    }
}

int main()
{
    pthread_t p_thread;
    int thr_id;
    int result;

    thr_id = pthread_create(&p_thread, NULL, t_function, NULL);
    if (thr_id < 0)
    {
        perror("thread create error : ");
        exit(0);
    }
    p_function();
    // 쓰레드 식별자 p_thread 가 종료되길 기다렸다가
    // 종료후에 리턴값을 받아온다.
	// t_function(NULL);
    //pthread_join(p_thread, (void *)&result);
    printf("thread join 실행됨: %d\n", result);
    printf("main() 종료\n");

    return 0;
}

실행 결과

두 스레드 모두 usleep이 있지만 번갈아가며 실행되지 않는다. 따라서,
usleep은 시그널처럼 왔다갔다 하는 용도가 아니라, 그냥 해당 시간만큼 소스를 비우겠다는 뜻이 된다.
이후의 시간에 도달하면 비워진 소스 중에 하나를 잡아서 작업을 이어서 진행한다.


실험 1-2 :
멀티스레드의 방식 중
진짜로 병렬적인지, 확인하고 싶었음

usleep을 주지 않았을 경우에는 어떻게 될지 확인

실행 결과

th2의 usleep을 줄이고,
main thread를 usleep없이 실행하고 1000이 되면 종료하도록 하면 thread2가 정상적으로 도는 것을 알게됨... usleep은 그냥
1. 리소스를 정말 비운다는 것.
2. Context Switching은 스레드 단에서도 일어난다.
(혹은 usleep이 없어도 일어난다 -> 스레드 스케쥴러에 의해)


실험2 :
이번엔 코드에서 시간만 반대로 메인 스레드의 시간을 500*500으로, thread2의 시간을 1000*1000으로
해서 메인이 시간을 더 빠르게 진행 시켜보았다.

작업 시간 : main thread < second thread

실행 결과

thread2가 미처 종료되지 않았음에도(실행 2 / 5)
main thread가 종료되니 모든 동작이 종료되는 것을 확인하였다.
하위 스코프에 있는 스레드는 그보다 상위 스코프의 스레드가 종료됨에 따라 작업에 상관없이 같이 종료된다.


pthread_join
특정 쓰레드가 종료되기까지 기다린 후 쓰레드가 종료되면 실행한다.
첫 번째 매개변수는 thread이고, 두 번째 매개변수는 해당 쓰레드 함수의 리턴값이 된다.
join된 쓰레드는 반납된 쓰레드로 간주하며, 모든 자원을 반납하게 된다.

*join은 목표한 스레드의 리턴값을 저장할 수 있다는 이점이 있다.

실험3
위에서 진행된 실험2 (작업시간 : main < 2nd thread)에서
join을 이용하면 2nd 스레드의 종료까지는 프로그램이 종료되는 걸 막을 수 있을 것으로 판단 후 진행

실험 결과

주석처리했던 join부분을 활성화하면 위와 같이 main이 먼저 끝나도 프로그램 종료가 아닌,
2nd 스레드의 종료까지 기다리게 된다.


pthread_mutex_lock

변수 자체에 대한 lock인줄 알았으나, 실제로 일어나는 현상은 코드의 잠금일 뿐이다.
잠겨있는 경우에 lock을 호출하면 usleep도 아닌, 그냥 대기상태로 담긴다.
즉 아래 코드가 있어도 실행되지 않도록 잠기는 효과를 가진다.
:::실행되면서 특정변수만 컨트롤하는 게 아님.

잠겨있는지만 먼저확인하고 해당 함수를 호출할 수만 있다면 잠겨있지 않는 경우에만 손대는 방식으로
훨씬 간단한 구현이 가능하겠지만 그렇게 되어있지는 않다.

하지만 코드가 잠긴다는 특성을 잘 이용하면 기존 방식의 가라(?)방식을 타파할 수 있다.
선 경합 방식


기존 방식 :
홀 / 짝에 대한 usleep의 차이를 통해 뮤텍스의 경합을 방지하는 방식
ㄴ> 스레드 시간이 001 이런식으로 밀려서 나오며, 바람직한 데드락 방지라고는 생각되지 않음.

index가 짝수면 우->좌, 홀수면 좌->우의 순으로 선 경합을 해주면
경합에서 진 철학자는 다른 포크에 대한 예약조차 걸지않는다. (mutex의 성질을 이용한 데드락 방지)
ㄴ>monitor solution && 루틴의 형성까지 가능하게됨

시행착오 1
이론상 아래의 코드는 문제가 없었으나 포크를 집는 부분에 루틴이 발생하지 않아
루프가 가능한 조건임에도
홀수의 경우 3회 루틴(3회 안에 모든 철학자 식사)을 충족하지 못했음.

	int	ft;
	int	ls;

	ft = philo->right;
	ls = philo->left;
	if (philo->id % 2)
	{
		ft = philo->left;
		ls = philo->right;
	}
	pthread_mutex_lock(&(data->forks[ft]));
	ft_philo_printf(arg, philo->id, "has taken a fork");
	pthread_mutex_lock(&(data->forks[ls]));

계속해서 action에서 원인을 찾고있었는데,
평가 중 무한 루프에 usleep이 없으면 스레드가 많을때 굉장한 부담이 있다는 이야기를 듣고
main의 무한 루프를 수정 -> 정상화 완료.

시행착오 2
3 300 90 60의 경우 기존 방식은 죽지않는 반면에 내가 수정한 방법은 죽었음.
포크를 집는 루틴이 만들어 지지 않아서 이거를 루틴화 하기 위해서 순서를 정해주었어야 함.
usleep으로 미루는 가라방식은 지양했으므로, eat action 즉 뮤텍스 접근이 크리에이트보다 빨랐어야 함.

int	ft_philo_eat(t_data *tdata, t_philo *philo, int phid)
{
	int	ft;
	int	ls;

	ft = phid + 1;
	ls = phid;
	if (phid % 2)
	{
		ft = phid;
		ls = phid + 1;
	}

ㄴ>원래는 philo->left, right를 통해 first, last를 찾아가는 방법으로 접근했는데, 구조체에서 한 번더
주소를 변경해야 뮤텍스를 들어감... 이 짧은 찰나에 뮤텍스를 할 수 있도록 해야하기 때문에 성능 향상을 위해
eat를 호출하는 부분에서 아예 인덱스째로 넘기는 방법을 고안.
ㄴ> 실제로 해결되었음.

시행 착오3
delay 함수 내에서 usleep을 50씩 주었었는데,
이것이 마이크로 초이기 때문에 스레드 하나당 while문을
도는 총량이 너무 많아져서 시스템에 부담을 주고 있었다.
그래서 포크를 unlock해줘야하는 스레드가 그냥 생략되거나
포크를 집어야하는 스레드가 집지 못하는 상황이 발생했었다.

usleep 단위가 너무 작기 때문에 500으로 줘도 무리가 없었으므로 이를 수정하였음.


deadlock 한 번 살펴볼 것
deadlock 참고자료

profile
42Seoul

0개의 댓글