반응형
멀티 쓰레드 사용시 참고 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/concurrency_parallel
thread_safety 참고 : http://www.joinc.co.kr/modules/moniwiki/wiki.php/man/12/thread%20safe
Reentrant 와 Thread_safety 의 차이점 : http://yesarang.tistory.com/214
POSIX 와 Thread-safety
재진입
POSIX.1에 기반한 C언어 함수들은 단일 쓰레드 프로세스 환경을 가정하고 만들어졌다. 재진입(Reentrancy)는 디자인 이슈가 아니었다. 그러므로 멀티 쓰레드 프로그래밍 환경에서 POSIX 함수가 재진입 가능할지를 보장할 수 없다. 멀티 쓰레드 환경에서는 사용하려는 함수가 재진입가능한지를 검토해야 한다.
예를 들어서 asctime함수는 프로세스의 메모리영역에 공간을 할당하고, 그에 대한 포인터를 돌려준다. 데이터 영역이 독립적이지 않기 때문에 다른 쓰레드에서 asctime함수를 호출하면, 프로세스 데이터가 변해 버리는 문제가 발생한다.
다음은 재진입하지 않는 함수들이다.
001 #include <stdio.h> 002 #include <unistd.h> 003 #include <stdlib.h> 004 #include <pthread.h> 005 #include <time.h> 006 007 #define MAX_THREAD_NUM 1 008 void *t_function(void *data) 009 { 010 time_t current_time ; 011 struct tm *mytm; 012 char *time_str; 013 014 current_time = time((time_t *)NULL); 015 mytm = localtime(¤t_time); 016 017 time_str = asctime(mytm); 018 printf("Child Thread Start Time : %s", time_str); 019 sleep(5); 020 } 021 022 int main(int argc, char **argv) 023 { 024 pthread_t p_thread[2]; 025 int thr_id; 026 int status; 027 int i = 0; 028 time_t current_time ; 029 struct tm *mytm; 030 char *time_str; 031 032 current_time = time((time_t *)NULL); 033 mytm = localtime(¤t_time); 034 time_str = asctime(mytm); 035 printf("Main Thread Start Time 1 : %s", time_str); 036 sleep(10); 037 038 for( i = 0; i < MAX_THREAD_NUM; i++) 039 { 040 thr_id = pthread_create(&p_thread[i], NULL, t_function, (void *)&i); 041 if (thr_id < 0) 042 { 043 perror("thread create error : "); 044 exit(0); 045 } 046 } 047 pthread_join(p_thread[0], NULL); 048 printf("Main Thread Start Time 2 : %s", time_str); 049 return 0; 050 } 051멀티 쓰레드 프로그램에서, 재진입을 보장하지 않는 함수를 사용했을 때 발생하는 문제를 보여주고 있다. 018과 048에서 time_str의 출력결과가 다르다. t_function 쓰레드함수에서 asctime을 호출하면서 메모리 할당된 영역의 데이터를 변경해 버렸기 때문이다.
- POSIX.1 프로세스 환경 관련 함수들 : getlogin(), ttyname()
- C언어 함수들 : asctime(), gmtime(), localtime()
- POSIX.1 시스템 데이터 베이스 함수 getgrgid(), getgrnam(), getpwuid(), getpwnam()
errno
errno는 external 전역 변수다. 그러므로 멀티 쓰레드 환경에서 에러를 검사하기 위한 목적으로 사용하기 힘들다. 최근에 호출한 함수의 errno값인지를 장담할 수 없기 때문이다.
재진입을 보장하기 위한 방법
- 재진입 가능한 버전의 함수 즉, _r이 붙은 함수를 사용한다.
- 뮤텍스등으로 전역 데이터에 대한 접근을 제어한다.
- gcc의 경우 _REENTRANT 정의해서 재진입을 보장할 수 있다. 재진입 가능한 함수와 그렇지 않은 함수가 존재 할때, 재진입 가능한 함수를 링크한다. 또한 errno와 같은 external 전역 변수를 각 쓰레드 마다 사용할 수 있도록 해준다.
# gcc -o myprog myprog.c -lpthread -D_REENTRANT
재진입 가능한 사용자 정의 함수 만들기
아래의 원칙을 지켜야 한다.
- 전역변수를 사용하지 않는다.
- 전역변수의 포인터를 반환하지 않는다. 대신 인자로 데이터를 넘겨받도록 한다. POSIX의 _r계열 함수가 이런 방식을 사용한다.
- 공유되는 자원은 접근제어를 한다.
- 재진입하지 않는 함수는 호출하지 않는다.
- 데이터 공간을 함수가 아닌 호출자가 제공하도록 한다.
병렬 프로그래밍에서 주의 해야할 문제들
ABA 문제
A 쓰레드와 B 쓰레드가 있다.
- A 쓰레드가 메모리의 값을 읽는다. 이런 저런 연산을 한다.
- B 쓰레드가 메모리의 값을 읽는다. 이런 저런 연산을 한다.
- A 쓰레드가 메모리에 연산 값을 쓴다.
- B 쓰레드가 메모리에 연산 값을 쓴다.
- A 쓰레드가 메모리의 값을 읽는다. 이런 저런 연산을 한다.
- A 쓰레드가 연산 값을 메모리에 쓴다.
- B 쓰레드가 메모리의 값을 읽는다. 이런 저런 연산을 한다.
- B 쓰레드가 메모리에 여산 값을 쓴다.
killer-tolerance
잠금을 얻은 쓰레드가 어떤 이유로 잠금을 풀지 않고 종료되더라도 다른 쓰레드에 영향을 미치면 안된다. 즉 스레드가 죽으면 자동적으로 잠금을 돌려줘야 한다. 만약 pthread우ㅢ mutex를 이용해서 잠금을 생성했다면, killer-tolerance를 걱정할 필요가 없다. 알아서 해제 시켜주기 때문이다. 다음의 코드를 테스트 해보자.
#include <stdio.h> #include <unistd.h> #include <pthread.h> int ncount; // 쓰레드간 공유되는 자원 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 쓰레드 초기화 void* do_loop(void *data) { int i; for (i = 0; i < 10; i++) { pthread_mutex_lock(&mutex); // 잠금을 생성한다. printf("loop1 : %d\n", ncount); ncount ++; if(i == 10) return; // 잠금을 풀지 않고 스레드를 종료한다. pthread_mutex_unlock(&mutex); // 잠금을 해제한다. sleep(1); } } void* do_loop2(void *data) { int i; // 잠금을 얻으려고 하지만 do_loop 에서 이미 잠금을 // 얻었음으로 잠금이 해제될때까지 기다린다. for (i = 0; i < 10; i++) { pthread_mutex_lock(&mutex); // 잠금을 생성한다. printf("loop2 : %d\n", ncount); ncount ++; pthread_mutex_unlock(&mutex); // 잠금을 해제한다. sleep(2); } } int main() { int thr_id; pthread_t p_thread[2]; int status; int a = 1; ` ncount = 0; thr_id = pthread_create(&p_thread[0], NULL, do_loop, (void *)&a); sleep(1); thr_id = pthread_create(&p_thread[1], NULL, do_loop2, (void *)&a); pthread_join(p_thread[0], (void *) &status); pthread_join(p_thread[1], (void *) &status); status = pthread_mutex_destroy(&mutex); printf("code = %d", status); printf("programing is end"); return 0; }
세마포어를 사용할 때, 혹은 파일 잠금이나 기타 IPC를 사용할 때 쓰레드의 종료가 잠금에 미치는 영향에 대해서 충분히 고려해야 한다.
async-signal-safety
특정 함수 A가 잠금을 얻은 상태에서, 시그널이 발생해서 시그널 핸들러가 실행됐다. 그런데, 시그널 핸들러가 함수 A를 다시 호출한다면, 락을 얻은 상태에서 다시 락을 얻을려고 실행하기 때문에 영원히 봉쇄될 것이다. 이런 경우가 자주 발생할 것 같지는 않다. 그냥 상식적으로 시그널 핸들러가 자신을 호출한 함수를 호출하지 않도록 작성하면 이 문제를 회피할 수 있을 것이다.
preemption-tolerance
잠금을 얻은 스레드가 휴면상태에 있을 때, 다른 스레드에 영향을 미치면 안된다. 다분히 논리적인 문제라고 할 수 있다. 즉 스레드는 마땅히 휴면상태에 있어야 할때, 휴면 상태에 놓여야 한다. 하지만 잠금을 얻은 상태에서 오랜 시간 임계영역에 머무르는 것은 바람직하지 않을 것이다.
processor heap
메모리를 할당 할때, 프로세스의 heap영역을 사용하는 것은 지양하자. 프로세스의 heap을 사용할 경우 false sharing 문제가 발생할 수 있다. 또한 자원 공유를 위한 락에는 많은 비용이 들어간다.
false sharing
멀티 코어 CPU에서 발생할 수 있는 문제다. 멀티 코어 CPU에서는 데이터를 word 단위로 읽어오는 대신 메모리 I/O 효율성을 위해서 cache-line로 읽어오는데, 이대 문제가 생길 수 있다. 두개의 메모리 연산이 동일한 캐쉬라인에서 실행될 경우, CPU<->Memory 버스 사이에서 하드웨어적인 락이 걸리는데, 이때 하드웨어적인 병목현상이 발생한다.
다음과 같은 방법으로 해결할 수 있다.
- OS가 관리해주는 메모리 단위인 페이지에서 같이 쓰는 데이터가 같이 올라가도록 할 것.
- 서로 다른 코어에서 접근할 수 있는 영역이면 캐쉬라인 단위로 떨어질 수 있게 적절히 패딩할 것
- 논리적으로 같이 사용되는 데이터라도, 성능을 위해서 서로 떨어트리는 것을 고려할 것.
#include <windows.H> #include <stdio.H> #include <tchar.H> volatile int data1; volatile int data2; DWORD CALLBACK TestThread1(void* /*arg*/) { for (int i = 0; i < 500000000; ++i) data1 = data1 + 1; return data1; } DWORD CALLBACK TestThread2(void* /*arg*/) { for (int i = 0; i < 500000000; ++i) data2 = data2 + 1; return data2; } int _tmain(int argc, _TCHAR* argv[]) { HANDLE thread[2]; SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); DWORD startTime = GetTickCount(); thread[0] = CreateThread(NULL, 0, TestThread1, (LPVOID)0, 0, NULL); thread[1] = CreateThread(NULL, 0, TestThread2, (LPVOID)0, 0, NULL); WaitForMultipleObjects(2, thread, TRUE, INFINITE); _tprintf(_T("%d\n"), GetTickCount() - startTime); return 0; }
반응형
'학습자료(~2017) > 리눅스' 카테고리의 다른 글
[리눅스] Linux Direct IO의 이해 (Synchronous IO와의 차이를 기반으로) - 펌 (0) | 2012.03.13 |
---|---|
[리눅스] 세마포어 - 자료 조사 (0) | 2012.03.12 |
[리눅스] pthread 함수 간단한 요약 정리, 자세한 참고사이트 링크 (1) | 2012.03.08 |
[리눅스] 시그널 종류와 발생 및 용도 (2) | 2012.03.08 |
[리눅스] 시간 관련 함수, 자료 정리 (0) | 2012.03.07 |