학습자료(~2017)/리눅스

[리눅스] Multi Thread, Mutl Processor, concurrency - parallel 관련 내용 정리 사이트

단세포소년 2012. 3. 8. 21:10
반응형

멀티 쓰레드 사용시 참고 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을 호출하면서 메모리 할당된 영역의 데이터를 변경해 버렸기 때문이다.

다음은 재진입하지 않는 함수들이다.
이 문제를 해결 하기 위해서 재진입 가능한 별도의 함수를 사용해야 한다. 함수에 _r이 붙은 함수는 재진입 가능한 버전의 함수임을 의미한다. asctime함수의 재진입 가능한 함수는 asctime_r함수다. 당연히 멀티 쓰레드 프로그래밍에서는 _r이 붙은 이름의 함수를 사용해야 한다.

errno

errno는 external 전역 변수다. 그러므로 멀티 쓰레드 환경에서 에러를 검사하기 위한 목적으로 사용하기 힘들다. 최근에 호출한 함수의 errno값인지를 장담할 수 없기 때문이다.

재진입을 보장하기 위한 방법

  1. 재진입 가능한 버전의 함수 즉, _r이 붙은 함수를 사용한다.
  2. 뮤텍스등으로 전역 데이터에 대한 접근을 제어한다.
  3. gcc의 경우 _REENTRANT 정의해서 재진입을 보장할 수 있다. 재진입 가능한 함수와 그렇지 않은 함수가 존재 할때, 재진입 가능한 함수를 링크한다. 또한 errno와 같은 external 전역 변수를 각 쓰레드 마다 사용할 수 있도록 해준다.

    # gcc -o myprog myprog.c -lpthread -D_REENTRANT 
     

재진입 가능한 사용자 정의 함수 만들기

아래의 원칙을 지켜야 한다.
  1. 전역변수를 사용하지 않는다.
  2. 전역변수의 포인터를 반환하지 않는다. 대신 인자로 데이터를 넘겨받도록 한다. POSIX의 _r계열 함수가 이런 방식을 사용한다.
  3. 공유되는 자원은 접근제어를 한다.
  4. 재진입하지 않는 함수는 호출하지 않는다.
  5. 데이터 공간을 함수가 아닌 호출자가 제공하도록 한다.

병렬 프로그래밍에서 주의 해야할 문제들

ABA 문제

A 쓰레드와 B 쓰레드가 있다.
  1. A 쓰레드가 메모리의 값을 읽는다. 이런 저런 연산을 한다.
  2. B 쓰레드가 메모리의 값을 읽는다. 이런 저런 연산을 한다.
  3. A 쓰레드가 메모리에 연산 값을 쓴다.
  4. B 쓰레드가 메모리에 연산 값을 쓴다.
이를 ABA 문제라고 한다. 정상적으로 연산이 되려면, 다음과 같은 흐름을 가져야 한다.
  1. A 쓰레드가 메모리의 값을 읽는다. 이런 저런 연산을 한다.
  2. A 쓰레드가 연산 값을 메모리에 쓴다.
  3. B 쓰레드가 메모리의 값을 읽는다. 이런 저런 연산을 한다.
  4. 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가 관리해주는 메모리 단위인 페이지에서 같이 쓰는 데이터가 같이 올라가도록 할 것.
  • 서로 다른 코어에서 접근할 수 있는 영역이면 캐쉬라인 단위로 떨어질 수 있게 적절히 패딩할 것
  • 논리적으로 같이 사용되는 데이터라도, 성능을 위해서 서로 떨어트리는 것을 고려할 것.

테스트 코드 : pthread로 바꿔서 테스트해봐야 겠다.
#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; 
} 
 
 
 
 
 
 
 
 
 
반응형