-
Notifications
You must be signed in to change notification settings - Fork 1
Pthread
뭐 말하자면 표준 쓰래드. 유닉스 계열 운영체제에서는 포함된다고 한다. 윈도우즈 용으로도 있다고 하는데, 써본적은 없다.
그동안 RTOS를 쓰면서, 쓰래드는 무한루프를 포함해야 한다는 생각을 가지고 있었는데, POSIX thread 또한 무한루프는 필수가 아닌 모양이다. 약간 괴리감이 있긴 했지만 일반 펌웨어 레벨에서는 main내부에 무한루프가 있는걸 생각하면 이해는 된다.
pthread_create(쓰래드 ID를 저장할 변수 포인터, 속성, 돌릴 함수(내용물), 쓰래드에 넘겨줄 인자);- 첫번째 인자 : 쓰래드의 ID를 저장할 변수의 포인터 값이 들리어간다. 변수는 pthread_t 로 선언하면 된다.
- 두번째 인자 : 속성값이 들어가는데, 주로 NULL값이 들어간다.
- 새번째 인자 : 쓰래드에서 실행될 내용물이 작성된 함수의 포인터를 넣어준다. 이때 함수의 반환값은 void 형이 아닌 void* 다.
- 내번째 인자 : 쓰래드에서 실행할 함수에서 인자로 전달된다.
정상적으로 쓰래드가 생성되면 0을 리턴한다.
pthread_exit(리턴값);쓰래드 내부에서 쓰래드를 종료할 때 사용하는 함수다. 몇가지 특징을 들면
- 호출된 쓰래드만 종료한다. 다른 쓰래드에는 영향이 안간다.
- return과 다르게 동적할당이 내부적으로 이루어진다.
어떻게 보면 main문도 하나의 쓰래드로 볼 수 있다. 예로 main에서 return으로 종료시 다른 쓰래드들은 별도의 인터럽트를 호출할 시간도 없이 종료되버린다. 때문에 그런걸 막기 위해 pthread_exit를 쓴다고 할 수 있다.
return과 다르게 어딘가 동적할당을 하는데, 나는 정확히 뭘 동적할당 하는지 모르겠다 ㅡㅡ;. 어쨌든 쓰래드를 많이 생성해도 이 동적할당된 전체 양은 변화하지 않는다고. 그리고 동적할당을 해재하지 않고 남겨두는데, 어차피 프로세스가 죽으면 다 정리될꺼기 때문이란다.
(청소 아주머니가 물 내려주실탠대 내가 X을 내릴 필요가 있는가? 라고 생각하는건가,,,)
pthread_join(쓰래드 ID를 저장한 변수, 리턴된 값을 저장할 변수 포인터);fork된 자식 프로세스를 waitpid로 기다리는 것과 꽤 비슷하다. 지정한 쓰래드 ID를 가진 쓰래드가 종료되는것을 기다린다. 기다리는 쓰래드 쪽에서 return 이나 pthread_exit 함수로 반환한 값은 두번째 인자에 넣어준 포인터의 변수가 넘겨받는다.
pthread_detach(쓰래드 ID를 지정한 변수);만약 지정한 쓰래드 ID를 가진 쓰래드가 detached 속성이라면 main쪽에서 쓰래드를 기다리지 않는다. detached 속성의 쓰래드는 detach 호출이 되면, 쓰래드를 종료할 때가 되었을 때 자기가 알아서 모든 자원을 해제한다. (자유방임주의?)
막상 쓰려니 이건 별로 쓰지 않는것 같기도,,,
이정도만 알아도 쓰래드의 기본적인 사용법은 아는 것이 아닐까. 몇가지 예제로 좀 익숙해져 보기로 하자.
대충 다음과 같이 쓰면 된다. 넘겨줄 인자가 정수 5라면,
#include <pthread.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
void *thread(void* data);
int main(void)
{
pthread_t threads[5];
int rc, t;
for(t = 0; t < 5; t++)
{
printf("In main: creating thread %d\n", t);
rc = pthread_create(&threads[t], NULL, thread, (void*)&t);
sleep(1); //쓰래드쪽에서 인자를 받을 시간을 충분히 주기
if(rc) //에러처리
{
printf("Thread creation error!\n");
exit(1);
}
}
pthread_exit(NULL);
return 0;
}
void *thread(void* data)
{
int id = *(int*)data;
sleep(id);
static int cnt = 0;
//printf("Hello I'm thread number %d \n", id);
printf("Hello I'm thread number %d, (cnt = %d)\n",id, cnt++);
pthread_exit(NULL);
}위 예제에서는 서로다른 5개의 쓰래드가 같은 내용의 일을 하도록 하였다. 속성값은 어지간해선 바꿀 일이 없었기 때문에 NULL 값으로 생성을 한다.
쓰래드에 인자가 전달될 때 약간의 시간을 주어야 한다고 한다. 안그러면 제대로 전달이 안된다고,,,
In main: creating thread 0
Hello I'm thread number 0, (cnt = 0)
In main: creating thread 1
In main: creating thread 2
Hello I'm thread number 1, (cnt = 1)
In main: creating thread 3
In main: creating thread 4
Hello I'm thread number 2, (cnt = 2)
실행해보면 인자가 제대로 전달 됨을 확인할 수 있다.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread(void* data)
{
int num = *(int*)data;
printf("num %d\n", num);
sleep(10);
return (void*)(num*num);
}
int main(void)
{
pthread_t th;
int tid;
int status;
int a = 100;
tid = pthread_create(&th, NULL, thread, (void*)&a);
pthread_join(th, (void*)&status);
printf("Thread join: %d\n", status);
return 0;
}이번엔 간단한 예제에 pthread_join을 이용해봤다. 생성되면서 인자로 100을 넘겨받고, 100*100을 하여 return한다.
이번엔 pthread_exit를 이용하지 않았는데, 이용하지 않아도 반환이 잘 되는것을 확인할 수 있었다. 성능 차이가 크게 나는지는 모르겠고,,, 쓰래드를 여러개 생성하면 느껴질 지 모르겠다.
return된 값은 pthread_join을 이용해서 status 라는 변수에 저장후 printf로 출력한다.
num 100
Thread join: 10000
앞서 pthread_create 의 속성 자리에 NULL값은 기본이라고 했는데, 속성을 변경할 수 있다.
속성은 다음 두가지가 있다.
- PTHREAD_CREATE_JOINABLE
- PTHREAD_CREATE_DETACHED
NULL을 넣을 경우 기본은 JOINABLE이다. 그 이외에 속성도 있는듯 하지만(유저 모드 쓰래드인지 커널 모드 쓰래드인지 등등,,,), 굳이 속성을 바꿀 필요가 있다면 아마 위의 속성을 주로 바꾸지 않을까.
pthread_attr_init(속성 자료형 포인터)여기서 자료형이라고 한 것이, 저기 자리에 속성 정보의 구조체가 들어가는데, 아무래도 객체로도 보는 것 같다(속성객체 라고 한다 카더라). 쨌든 인자로는 pthread_attr_t 라는 자료형을 이용한다. 이용할 땐
pthread_attr_t attr;
pthread_attr_init(&attr);위 처럼 이용하면 된다.
다른 속성 함수(scope 관련 커널 모드인지 등등,,,)도 있는데, 여기서는 pthread_attr_setdetachstate 라는 함수를 언급할 것이다.
pthread_attr_setdetachstate(속성 자료형 포인터, 속성이름)속성을 joinable 인지 detached 인지 설정할 수 있는 함수다. 이용은 다음과 같이 초기화와 함께 사용하면 된다.
pthread_attr_t attr;용
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);PTHREAD_CREATE_JOINABLE
- pthread_join 함수와 같이 이용한다.
- joinable 속성에서 pthread_join 호출시 쓰래드가 끝날때까지 기다려준다.
PTHREAD_CREATE_DETACHED
- pthread_detach 함수와 같이 이용한다.
- detached 속성에서 pthread_detach 호출시 쓰래드가 끝날때까지 기다리지 않는다.
- 쓰래드는 자기가 끝나면 알아서 자원을 모두 해제한다.
위의 속성 설정까지 한 후 pthread_create를 이용해 쓰래드를 생성하면, 더이상 속성객체는 필요가 없어진다.
pthread_attr_destroy(속성 자료형 포인터)속성객체 attr이 있다면, 이용은 다음과 같이 하면 된다.
pthread_attr_destroy(&attr);#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* work(void *null)
{
int i;
double result = 0.0;
for(i = 0; i < 100000; i++)
result +=(double)random();
printf("result = %e\n", result);
pthread_exit((void*)0);
}
int main(void)
{
pthread_t thread[3];
pthread_attr_t attr;
int rc, t;
void* status;
pthread_attr_init(&attr); //속성객체 생성
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); //joinable로 속성 설정
for(t = 0; t < 3; t++)
{
printf("Creating thread %d\n", t);
rc = pthread_create(&thread[t], &attr, work, NULL);
}
pthread_attr_destroy(&attr); //속성제거
for(t = 0; t < 3; t++)
{
rc = pthread_join(thread[t], &status);
printf("Completed join with thread %d status = %ld\n", t, (long)status);
}
pthread_exit(NULL);
return 0;
}Creating thread 0
Creating thread 1
Creating thread 2
result = 1.073075e+14
Completed join with thread 0 status = 0
result = 1.075923e+14
Completed join with thread 1 status = 0
result = 1.072782e+14
Completed join with thread 2 status = 0
사실 joinable 은 기본 설정이기 때문에 굳이 별도로 설정할 필요는 없지만, 연습삼아 한번 설정을 해봤다.
joinable로 쓰래드 생성후, 각 쓰래드는 랜덤한 값을 십만번 더한 후 결과를 출력한다.
메인 문에서는 쓰래드 0번 부터 2번까지 순서대로 끝날것을 기대하고 pthread_join을 한다.
쓰래드간에 서로 공유하는 자원이 있다. 근대 이 공유자원에 작업을 하고 있는데, 가끔 다른 쓰래드가 치고 들어와서 이상한 짓을 하고 간다. 그런걸 방지하기 위한 뮤텍스다. 다른 RTOS 에서도 뮤텍스나 세마포어는 기본적으로 지원했기 때문에 익숙했다.
pthread_mutex_init(뮤텍스 자료형 포인터, 속성)여기서도 뮤텍스객체 라고 하던데, c에서 객체라고 하니 좀 이상한 느낌도 든다(객체라는게 개념적인거니 뭐,,,). 자료형이 pthread_mutex_t인 구조체(객체라고 해야하냐,,,)를 사용한다. 속성은 다른 속성이 있다는것 같은데, 어차피 크리티컬 섹션을 만드는 것에서만 쓰기 때문에,,, 기본 모드인 fast 모드만 사용할 것 같다. 기본 모드로 사용할려면 속성에 NULL값을 넣어주면 된다.
pthread_mutex_t 로 선언된 변수는 전역변수로 사용해야 한다. 왜냐하면 쓰래드 함수 내부에서도 사용해야 하기 때문에,,,
사용은
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);과 같이 사용하면 된다.
pthread_mutex_destroy(뮤텍스 자료형 포인터)
작업할 쓰래드가 모두 끝나고, 뮤텍스가 더이상 쓸모 없을때 지우는 역할이다. 딱히 더 언급할게 없는것 같다.
pthread_mutex_destroy(&mutex);사용법도 뭐,,,
pthread_mutex_lock(뮤텍스 자료형 포인터)
pthread_mutex_unlock(뮤텍스 자료형 포인터)뮤텍스로 크리티컬 섹션 만드는건 크게 어렵지 않다. 뮤텍스가 뭔지만 알아도 이해가 될 것이다. 어떤 값이 있는데, 상태가 '사용중'과 '사용안함'이 있다. 만약 사용안함 상태라면 사용을 위해 lock을 걸어 사용중으로 바꾼다. 사용이 끝나면 unlock 해서 사용안함으로 바꿔준다.
만약 val 이란 변수를 쓰래드간에 공유하고, 크리티컬 섹션을 만들어 변경해야 한다면
pthread_mutex_lock(&mutex);
val++;
pthread_mutex_unlock(&mutex);이와 같이 lock과 unlock 사이에 변경할 것들을 끼워 넣으면 그만.
#define NITERS 100000000
#include <stdio.h>
#include <pthread.h>
unsigned int cnt = 0;
void* count(void *data);
pthread_mutex_t mutex; //뮤텍스 객체 생성
int main(void)
{
pthread_t tid1, tid2;
pthread_mutex_init(&mutex, NULL); //뮤텍스 설정 초기화
pthread_create(&tid1, NULL, count, NULL);
pthread_create(&tid2, NULL, count, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&mutex); //뮤텍스 설정 삭제
if(cnt != (unsigned)NITERS*2)
{
printf("BOOM! cnt = %d\n", cnt);
}
else
{
printf("OK! cnt = %d\n", cnt);
}
return 0;
}
void* count(void* data)
{
int i;
for(i = 0; i < NITERS; i++)
{
pthread_mutex_lock(&mutex);
cnt++;
pthread_mutex_unlock(&mutex);
}
}쓰래드 두개를 만들어 cnt라는 전역변수에 각각 NITERS(100000)만큼 더하는 간단한 예제다. 쓰래드 두개가 각각 더하고 있으므로 결과는 NITERS의 두배에 해당하는 값이 나와야 한다. 뮤텍스를 사용하지 않는다면 상호배제가 안되어 순서가 꼬여 제대로 두배가 안된다.
뮤텍스를 사용 안했을때, 계산은 빨리 끝났지만 자꾸 이상한 결과값이 나왔다. 어셈블리어 레벨로 들어가서 확인해보면 왜 그런지 이해할 수 있는데, 그냥 설명해보기로 하자. cnt++에 해당하는 내용이 대충 아래같은 순서를 가질 것이다.
- cnt의 값을 레지스터에 복사
- 레지스터에서 값을 1더함
- 그 값을 다시 cnt에 복사
뭐 이런 순서다. 어셈블리어를 나열하라고 하면 음,,, (군생활로 빠가된 머리로 과연 어셈블리어를 보고 때려치지 않을 수 있을까?)
쨋든 복사하고 복사하는 과정에서 다른 쓰래드가 치고 들어와서 변경해버리면 당연히 이상한 값이 되버릴거다. 그래서 뮤텍스가 필요하다.
(에초에 전역변수를 사용하는게 마음에 안들긴 하지만 ㅡㅡ;)
저 소스대로 뮤텍스를 사용하면 속도는 좀 크게 떨어지긴 해도 정상적인 출력은 나온다. for문 밖에 두면 좀 더 빨리지긴 하겠지.