Philosophers
필요한 변수:
number_of_philosophers
철학자 수(+ 포크의 수)
time_to_die
지난 번 식사로부터 식사를 마칠 때까지 남은 시간.
time_to_eat
밥 먹는 데 걸리는 시간. 두 개의 포크를 사용해야 한다.
time_to_sleep
잠자는 데 걸리는 시간.
number_of_times_each_philospher_must_eat
옵션이며, 식사 횟수가 이 변수의 값과 같아지면 프로그램 종료.
Program 1 - 이해가 필요한 함수들
usleep
gettimeofday
pthread_create
pthread_detach
pthread_join
pthread_mutex_init
pthread_mutex_destroy
phthread_mutex_lock
pthread_mutex_unlock
usleep
설명
usleep은 코드 실행을 늦추는 함수이다.
헤더는 <unistd.h>,
함수의 형태는 void usleep(unsignd long useconds)
이다.
마이크로 초를 기반으로 하며, 밀리초로 바꾸려면 원하는 밀리초 * 1000
을 해주면 된다.
(참고로 1000밀리초는 1초, 1000 * 1000 마이크로초도 1초이다.)
ex
#include <stdio.h>
#include <unistd.h>
int main()
{
while (true)
{
printf("1초마다 문자열을 출력합니다\n");
usleep(1000*1000);
}
}
// ./a.out 으로 실행시 1초마다 메시지가 출력된다.
ref - http://forum.falinux.com/zbxe/index.php?mid=C_LIB&document_srl=564661
gettimeofday
설명
gettimeofday() 함수는 마이크로초 단위의 시간을 되돌려준다.
헤더 및 형식
#include <sys/time.h>
#include <unistd.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
첫 번째 인자인 tv
는 현재 시스템 시간을 저장하기 위한 구조체이다.
두 번째 인자인 tz
는 타임존을 설정하기 위해 사용되는 구조체이다.
해당 구조체들은 다음과 같은 값을 갖는다.
struct timeval // tv
{
long tv_sec; // 초
int tv_usec; // micro초
}
struct timezone //tz
{
int tz_minuteswest; // 그리니치 기준 서측으로 분(minute) 차(difference)
int tz_dsttime; // DST 보정타입. 일광 절약시간...?
}
(정말 다행히도(?) tz는 현재 사용되지 않아서 NULL로 처리하는 게 일반적이라고 한다.)
반환값
성공 시 0, 실패 시 -1 리턴.
예제1
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
struct timeval mytime;
gettimeofday(&mytime, NULL);
printf("%ld:%d\n", mytime.tv_sec, mytime.tv_usec);
printf("%ld:%d\n", mytime.tv_sec * 1000000, mytime.tv_usec);
printf("relative time is: %ld\n", mytime.tv_sec * 1000000 + mytime.tv_usec);
return (0);
}
/*
1618818045:394638
1618818045000000:394638
relative time is: 1618818045394638
*/
예제2
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
struct timeval startTime, endTime;
double diff_tv_sec;
double diff_tv_usec;
gettimeofday(&startTime, NULL);
usleep(1000 * 1000);
gettimeofday(&endTime, NULL);
diff_tv_sec = ( endTime.tv_sec - startTime.tv_sec );
diff_tv_usec = ( endTime.tv_usec - startTime.tv_usec ) / 1000000;
printf("%f seconds\n", diff_tv_sec); // f는 (double로) 형변환해 출력.
printf("%f micro seconds\n", diff_tv_usec);
return 0;
}
// 결과
// 1.000000 seconds
// 0.000000 micro seconds
필자는 과제에서 이렇게 사용!
size_t relative_time(size_t time_start)
{
struct timeval current;
gettimeofday(¤t, 0);
return (current.tv_sec * 1000 * 1000 + current.tv_usec - time_start);
}
ref -
https://www.joinc.co.kr/w/man/2/gettimeofday
pthread_create
pthread
pthread란, POSIX thread의 약자로 유닉스 계열 POSIX 시스템에서 스레드를 편하게 만들 수 있게 도와주는 API이다.
헤더 및 사용형식
pthread사용을 위해서는 <pthread.h>
를 include 해야 한다.
그리고 pthread_create는 쓰레드를 생성하는 함수로서 다음의 형식을 갖는다.
int pthread_create(phtread_t *thread, const phtread_attr_t *attr, void *(*start_routine)(void *), void *arg);
여기서 첫 번째 매개변수인 pthread_t *thread
는 쓰레드 식별자로서 생성된 스레드를 담을 쓰레드의 주소 정도로 생각하면 되겠다.
두 번째 매개변수인 const phread_attr_t *attr
은 쓰레드 특성을 지정하기 위해 이용하는데, 대개 NULL 처리 해버리니 크게 신경쓰지 않아도 될 듯하다.
세 번째 매개변수인 void *(*start_routine)(void *)
은 thread가 실행되었을 때 시작할 함수이다.
네 번째 매개변수인 void *arg
는 세 번째 매개변수인 함수의 인자로 들어갈 인자이다.
pthread_create를 한번 실행시키면 하나의 스레드가 추가로 생성되기 때문에 총 두 개의 쓰레드(main쓰레드, 새로 생성한 쓰레드)가 동작하게 된다.
예제
이제 예제를 통해 한번 살펴보자.
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void *p_function(void * data);
int main(void)
{
pthread_t pthread;
int thr_id;
int status;
char p1[] = "thread_created";
char p2[] = "thread_main";
thr_id = pthread_create(&pthread, NULL, p_function, (void*)p1);
if(thr_id < 0)
{
perror("pthread0 create error");
exit(EXIT_FAILURE);
}
p_function((void *)p2);
printf("created thread id: %x\n", pthread);
printf("end\n");
return 0;
}
void *p_function(void * data)
{
pthread_t tid; // thread id
tid = pthread_self();
char* thread_name = (char *)data;
int i = 0;
while(i < 3)
{
printf("thread name : %s, tid : %x\n", thread_name, (unsigned int)tid);
i++;
usleep(1000 * 1000);
}
}
위 예제에서는 pthread_create
를 통해 쓰레드를 생성함과 동시에 p_function
함수를 호출하여 인자로 준 thread_name
과 생성된 thread_id
를 출력하고 있다. 여기서 usleep1초를 기다리게 만드는데, 어떤 쓰레드에서 usleep이 호출되면 다른 쓰레드에서 일을 하게 된다. 결국 main_thread와 우리가 생성한 thread가 번갈아가며 실행되는 것이다.
결과는 다음과 같다.(컴파일: gcc pthread_create.c -pthread
)
thread name : thread_main, tid : 12b75dc0
thread name : thread_created, tid : 65a8000
thread name : thread_main, tid : 12b75dc0
thread name : thread_created, tid : 65a8000
thread name : thread_main, tid : 12b75dc0
thread name : thread_created, tid : 65a8000
created thread id: 65a8000
end
ref - https://m.blog.naver.com/whtie5500/221692793640
pthread_join
헤더 및 사용형식
#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);
설명
특정 쓰레드가 종료되기까지 기다린 후 쓰레드가 종료되면 실행한다.
첫 번째 매개변수는 thread이고, 두 번째 매개변수는 해당 쓰레드 함수의 리턴값이 된다.
join된 쓰레드는 반납된 쓰레드로 간주하며, 모든 자원을 반납하게 된다.
예제 살펴보기
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void *t_function(void *param)
{
printf("쓰레드 함수 실행\n");
for (int i = 1; i <= 5; i++)
{
usleep(1000 * 1000);
printf("쓰레드 함수 실행 중..(%d / 5)\n",i);
}
printf("쓰레드 함수 종료\n");
return (void *)2147483647; // result에 담길 값.
}
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_thread 가 종료되길 기다렸다가
// 종료후에 리턴값을 받아온다.
pthread_join(p_thread, (void *)&result);
printf("thread join 실행됨: %d\n", result);
printf("main() 종료\n");
return 0;
}
결과
쓰레드 함수 실행
쓰레드 함수 실행 중..(1 / 5)
쓰레드 함수 실행 중..(2 / 5)
쓰레드 함수 실행 중..(3 / 5)
쓰레드 함수 실행 중..(4 / 5)
쓰레드 함수 실행 중..(5 / 5)
쓰레드 함수 종료
thread join 실행됨: 2147483647
main() 종료
ref -
https://bitsoul.tistory.com/160
pthread_detach
헤더 및 사용형식
# include <pthread.h>
int phtread_detach(phtread_t th);
설명
특정 쓰레드를 해제한다.
일반적으로 pthread_create를 통해 생성한 쓰레드는 쓰레드 종료 시점에 자동으로 자원이 해제되는 게 아니다.
이를 해제해줄 수 있는 것이 pthread_join과 pthread_detach이다.
pthread_join vs phtread_detach 🌟
pthread_join과 pthread_detach 의 차이는 join의 경우 쓰레드가 종료될 때까지 기다린다
는 것이고, detach는 만나는 즉시 쓰레드를 종료시켜버린다
는 것이다. 그리고 더 중요한 차이는 join은 쓰레드가 종료될 때까지 기다리므로 다른 쓰레드가 실행하는 함수가 이루어질 수 없을 수 있다
는 것이고 detach는 다른 쓰레드가 실행하는 함수가 실행될 수 있다는 것
이다.
좀 더 직관적으로 이해하기 위해 아래 예제를 한 번 살펴보자.
예제
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void *t_function(void *param)
{
for (int i = 1; i <= 5; i++)
{
usleep(1000);
// 여기서 main쓰레드로 넘어감.
// main에서는 그 다음 명령이 detach이므로
// 그 시점에 바로 쓰레드 종료.
printf("쓰레드 함수 실행 중..%d/5\n",i);
}
printf("쓰레드 함수 종료\n");
return (void *)2147483647;
}
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);
}
pthread_detach(p_thread);
printf("main() 종료\n");
return 0;
}
결과
main() 종료
결과가 저 한 줄만 출력된 이유는 Thread함수에서 usleep이 호출될 때 main쓰레드로 넘어가게 되는데 main쓰레드에서는 detach를 만나자 마자 생성한 thread를 종료 및 반납해버리므로 main() 종료
라는 문구만 출력되고 있다.
만약 join을 썼다면 아래와 같은 문구가 출력되었을 것이다.
쓰레드 함수 실행 중..1/5
쓰레드 함수 실행 중..2/5
쓰레드 함수 실행 중..3/5
쓰레드 함수 실행 중..4/5
쓰레드 함수 실행 중..5/5
쓰레드 함수 종료
main() 종료
그런데 detach와 join의 차이를 확인할 수 있는 더 중요한 예제는 바로 다음이다.
일단 예제를 다음과 같이 바꾸었다.
main문이 종료되지 않게 무한루프를 추가했고, 쓰레드를 두 개로 나누었다.
아래 예제를 돌려보고 결과를 확인해볼텐데, 그 결과에 대해 미리 요약하자면
join은 쓰레드가 끝나기 전까지 main 쓰레드가 동작할 수 없게 만들고, detach는 동작할 수 있게 만든다
는 것이다.
예제1 - detach
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void *t_function(void *param)
{
for (int i = 1; i <= 5; i++)
{
usleep(1000 * 1000 * 2); //2초 대기
printf("%s: ", (char *)param);
printf("쓰레드 함수 실행 중..%d/5\n",i);
}
printf("쓰레드 함수 종료\n");
return (void *)2147483647;
}
int main()
{
pthread_t p_thread1;
pthread_t p_thread2;
int thr_id1;
int thr_id2;
thr_id1 = pthread_create(&p_thread1, NULL, t_function, "thread1");
thr_id2 = pthread_create(&p_thread2, NULL, t_function, "thread2");
if (thr_id1 < 0 || thr_id2 < 0)
{
perror("thread create error : ");
exit(0);
}
pthread_detach(p_thread1);
pthread_detach(p_thread2);
// pthread_join(p_thread1, 0);
// pthread_join(p_thread2, 0);
int s = 0;
while (42)
{
printf("%d초 경과\n", s++);
usleep(1000 * 1000);
}
printf("main() 종료\n");
return 0;
}
결과 : main 쓰레드가 돌리는 while(42) 부분과 t_function이 동시에 돌아간다.
0초 경과
1초 경과
thread1: 쓰레드 함수 실행 중..1/5
thread2: 쓰레드 함수 실행 중..1/5
2초 경과
3초 경과
thread1: 쓰레드 함수 실행 중..2/5
4초 경과
thread2: 쓰레드 함수 실행 중..2/5
5초 경과
thread1: 쓰레드 함수 실행 중..3/5
6초 경과
thread2: 쓰레드 함수 실행 중..3/5
7초 경과
thread1: 쓰레드 함수 실행 중..4/5
thread2: 쓰레드 함수 실행 중..4/5
8초 경과
9초 경과
thread1: 쓰레드 함수 실행 중..5/5
쓰레드 함수 종료
thread2: 쓰레드 함수 실행 중..5/5
쓰레드 함수 종료
10초 경과
11초 경과
12초 경과
13초 경과
...
예제2. - join. (위 예제에서 주석의 위치만 바뀜.)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void *t_function(void *param)
{
for (int i = 1; i <= 5; i++)
{
usleep(1000 * 1000 * 2); //2초 대기
printf("%s: ", (char *)param);
printf("쓰레드 함수 실행 중..%d/5\n",i);
}
printf("쓰레드 함수 종료\n");
return (void *)2147483647;
}
int main()
{
pthread_t p_thread1;
pthread_t p_thread2;
int thr_id1;
int thr_id2;
thr_id1 = pthread_create(&p_thread1, NULL, t_function, "thread1");
thr_id2 = pthread_create(&p_thread2, NULL, t_function, "thread2");
if (thr_id1 < 0 || thr_id2 < 0)
{
perror("thread create error : ");
exit(0);
}
//pthread_detach(p_thread1);
//pthread_detach(p_thread2);
pthread_join(p_thread1, 0);
pthread_join(p_thread2, 0);
int s = 0;
while (42)
{
printf("%d초 경과\n", s++);
usleep(1000 * 1000);
}
printf("main() 종료\n");
return 0;
}
결과: (main 쓰레드가 돌아가지 않는다 !)
thread1: 쓰레드 함수 실행 중..1/5
thread2: 쓰레드 함수 실행 중..1/5
thread1: 쓰레드 함수 실행 중..2/5
thread2: 쓰레드 함수 실행 중..2/5
thread1: 쓰레드 함수 실행 중..3/5
thread2: 쓰레드 함수 실행 중..3/5
thread1: 쓰레드 함수 실행 중..4/5
thread2: 쓰레드 함수 실행 중..4/5
thread1: 쓰레드 함수 실행 중..5/5
쓰레드 함수 종료
thread2: 쓰레드 함수 실행 중..5/5
쓰레드 함수 종료
0초 경과
1초 경과
2초 경과
3초 경과
4초 경과
...
이 둘의 차이를 비교하는 것은 philosopher 과제를 하는 데에 있어서 필수다.
한번 잘 생각해보고 어떻게 사용할 수 있을지 알아내보자.
ref -
https://www.joinc.co.kr/w/man/3/pthread_detach
https://bitsoul.tistory.com/161
pthread_mutex_init & phthread_mutex_lock & pthread_mutex_unlock
mutex란?
mutex는 여러 개의 쓰레드가 공유하는 데이터를 보호하기 위해 사용되는 도구이다.
어떤 코드영역에 대해 한 번에 하나의 쓰레드만
실행가능하도록 만듦으로써 공유되는 자원을 공유한다.
무슨 말인지 아직 이해가 가지 않을 것이다.
아래 예제를 통해 좀더 살펴보자.
참고로 하나의 쓰레드만 접근할 수 있는 영역을 critical section
이라고 한다.
pthread_mutex_init
먼저 mutex를 init하는 것부터 알아보자. 이것을 해주어야 mutex를 사용할 수 있게 된다.
phtread_mutex_init 함수를 사용한다.
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
파라미터
pthread_mutex_t *mutex
초기화 할 mutex 객체
const pthread_mutexattr_t * attr
mutex의 특성을 맞추기 위해 설정함. 기본적으로 NULL임.
종류로 fast, recurisev, error checking이 있는데 디폴트 값은 fast이다.
에러 ERROR
- EINVAL : attr이 이상한 값이 들어옴
- ENOMEM : 다른 뮤텍스에서 사용한 쓰레드
TIP
정적으로 할당하고 싶을 땐 아래와 같이 실행하면 됨
pthread_mutex_t mutex = PTHREAD_MUTX_INITIALIZER;
Pthread_mutex_lock & unlock
이 두 가지 함수는 mutex 임계 구역 진입 시 그 코드 구역을 잠그고 여는 역할을 해주는 함수들임.
한 쓰레드에서 lock함수를 이용해 닫아줬다면 다른 코드는 그 구역에 들어가기 위해 unlock할 때까지 기다려야 한다.
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
인자인 mutex는 잠그거나 해제시켜줄 쓰레드.
에러
- pthread_mutex_lock
- 에러 시 에러코드
- EINVAL : mutex 유효하지 않음
- EDEADLK : 데드락 조건이 발생
- 에러 시 에러코드
- pthread_mutex_unlock
- 에러 시 에러코드
- EINVAL : mutex 유효하지 않음
- EPERM : 현재 쓰레드가 잠겨져있지 않음
- 에러 시 에러코드
예제
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
pthread_mutex_t mutex;
int cnt = 0;
void *count(void *arg){
int i;
char* name = (char*)arg;
// pthread_mutex_lock(&mutex);
//======== critical section =============
cnt = 0;
for (i = 0; i < 10; i++)
{
printf("%s cnt: %d\n", name,cnt);
cnt++;
usleep(1);
}
//========= critical section ============
// pthread_mutex_unlock(&mutex);
}
int main()
{
pthread_t thread1,thread2;
pthread_mutex_init(&mutex,NULL);
pthread_create(&thread1, NULL, count, (void *)"thread1");
pthread_create(&thread2, NULL, count, (void *)"thread2");
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex);
}
위 예제 코드를 실행하면 아래와 같이 나오는데, phtread_mutex_lock, unlock 부분의 주석을 해제하고 실행한 결과를 비교하면 다음과 같다.
// 주석 해제 전
thread2 cnt: 0
thread1 cnt: 0
thread2 cnt: 1
thread1 cnt: 3
thread2 cnt: 4
thread1 cnt: 5
thread2 cnt: 5
thread1 cnt: 7
thread2 cnt: 8
thread2 cnt: 9
thread1 cnt: 9
thread2 cnt: 10
thread1 cnt: 11
thread2 cnt: 13
thread1 cnt: 14
thread2 cnt: 15
thread1 cnt: 16
thread2 cnt: 17
thread1 cnt: 18
thread1 cnt: 19
// 주석 해제 후 (mutex_lock, unlock 적용 후)
thread2 cnt: 0
thread2 cnt: 1
thread2 cnt: 2
thread2 cnt: 3
thread2 cnt: 4
thread2 cnt: 5
thread2 cnt: 6
thread2 cnt: 7
thread2 cnt: 8
thread2 cnt: 9
thread1 cnt: 0
thread1 cnt: 1
thread1 cnt: 2
thread1 cnt: 3
thread1 cnt: 4
thread1 cnt: 5
thread1 cnt: 6
thread1 cnt: 7
thread1 cnt: 8
thread1 cnt: 9
그 외) Semaphore와 Process
Semaphore에 대해 잘 정리해놓은 joockim의 노션을 소개한다.
여기 링크를 클릭하면 들어갈 수 있다.
그리고 바로 다음 글에서는 seongwpa님이 정리하신 semaphore + process 글에 살짝의 내 생각만 얹은 글을 쓸 예정이다.
다음 글에서 만나요우~
ref-
https://reakwon.tistory.com/98
'Code 42 > Philosophers' 카테고리의 다른 글
42 Philosophers(철학자 문제) 필수지식 정리 - 2 (0) | 2021.05.01 |
---|---|
Philosophers 해석 (0) | 2021.02.09 |
최근댓글