728x90
반응형

Thraed 간에 동기화 시키는 mechanism에 대해서 알아보고 OS에서 제공하는 system call 함수를 살펴보면서 동기화 문제와 해결방안, 그에 대한 idea에 대해서 얘기하겠다. Multi thread를 활용할 때 프로그램 성능은 향상되지만 thread들 간에 충돌문제가 발생할 수 있으니 thread간에 충돌이 생기지 않도록 관리하는 방법도 알아보겠다. 

 

Mutex

: Mutex라고 하는 것은 mutual exclusion의 약자이다. mutual exclusion은 번역을 하면 상호배제. 즉, 서로 배제를 하겠다는 뜻이다. 어떤 이유 때문에 이런 조건이 필요하냐 하면 thread간에 발생할 수 있는 충돌문제 때문이다. 

 예를 들어 프로그램에서 어떤 Resource가 있는데 (ex. 전역변수) 어느 시점에 A라는 thread와 B라는 thread가 이 변수에 서로 다른 값을 입력을 할려고 하는 것이다. 즉, 동시에 update 할려고 하는 것이다. 이 때, 충돌문제가 발생한다. 문제를 막기 위해서는 solution을 구현하기가 어려운데, 생각으로는 동시에 access하는 것을 막으면 될 것이라는 생각이 든다. Thread가 한번에 한 thread씩 공유변수를 access하게끔, 동시에 access 하는 부분을 OS level에서 막을 수 있는 mechanism을 제공하자. 먼저 요청한 thread가 resource를 access 했다면 다른 thread는 기다려야 한다.

 

 Mutual exclusion을 만족하는 영역을 critical section이라고 한다. Thread 하나가 critical section에 들어오면 다른 Thread는 못들어오는 것이다. 이 영역에는 하나의 Thread만 들어올 수 있다. Mutex 실행하는 변수를 사용해서 기본적인 thread간의 동기화 mechanism을 사용할 수 있는 수단으로 사용할 수 있다. 그걸 통해서 공유 data를 보호하고 동시에 access하는 것을 막을 수 있다. 즉, 순서대로 data를 access하면 충돌문제를 피할 수 있다.

 

그래서 Mutex variable이라는 객체가 lock의 개념으로 작동한다. 공유 변수에 access하고싶으면 lock을 먼저 호출하고 양쪽 thread가 lock을 호출하면 먼저 호출한 thread에게 lock을 풀어주고 그 thread가 열쇠를 갖고 임계영역에 들어오고 들어오지 못한것은 lock에 대한 waiting queue에 들어가서 대기하게 된다. (lock에 대한 waiting queue가 또 따로 있다) lock을 걸고 해제하는 작동은 Mutex variable을 통해서 할 수 있다. Mutex variable은 Mutex lock mechanism에 해당하는 system call함수를 제공해 주는 것이다. 

 

 오직 하나의 thread만 Mutex variable을 소유할 수 있다(lock을 얻을 수 있다) lock을 가진 thread가 임계 영역에 들어가서 공유데이터를 access하고 다 사용하고 나면 반드시 자기가 가지고 있는 lock권한을 해제를 해야 한다. lock을 시스템에 반환하고 넘어가야 OS는 lock을 받아서 다음차례의 thread가 들어와서 data를 access할 수 있는 것이다.

 OS는 프로그래머가 lock을 요청하고 lock을 해제할 수 있는 서비스만 mechanism으로 제공하는 것이고 개발자가 thread를 잘 분석해서 충돌영역이 있는 부분을 lock, unlock을 잘 사용해야 한다. Lock을 요청한 thread가 여러개가 있을 때 나머지 thread들이 waiting queeue에 대기하고 있는데 대기중인 thread가 계속 대기하면 문제가 생기고 critical section의 요구사항에도 맞지가 않는다. Mutex라고 하는 기본 mechanism을 이용해서 "race" condition(충돌 문제로 생기는 문제, 서로 업데이트 할려고 하는 상황)을 막아야 한다.

 

 Race condition은 여러 thread가 동시에 공유 데이터를 access할려고 해서 발생하는 문제이다. Race condition을 실제 상황에 적용해보자. 예를 들어, 은행에 계좌가 있는데 한 은행 계좌에 여러 군데서 그 계좌에 돈을 집어넣는 서로 다른 thread에서 집어넣을려고 하는 상황이 발생하는 것이다. 계좌 소유자가 ATM기에 가서 200달러를 집어넣을려고 한다. 그런데 그것과 거의 동시에 다른 은행 쪽 지점에서 뭔가 수익이 생겨서 어떤 식으로든 이 계좌로 200달러를 입금을 할려고 요구하면 transaction이 같은 계좌에 동시에 발생한 것이다. 두 thread는 계좌의 잔액을 동시에 update하려고 하는 것이다. 1번 thread는 200달러를 더할려고 하는 것이고 2번 thread도 계좌에 업데이트 할려고 하는 것이다. 정상적으로 수행이 되었다면 task가 끝나고 나서 잔액을 1400달러가 되어야 하는데 동시에 제어없이 access하게 되면 1400달러가 아닌 결과가 발생할 수도 있게 된다. 현재 잔액 정보를 읽어서 update한 값에 계산을 수행하고 다시 write하는 과정을 거쳐야 한다. 

 이 문제를 막기 위해 동시에 접근하는 부분을 critical section으로 막아서 access하는 것을 먼저 요청한 thread에게 권한을 주는 것으로. update 다 끝내고 나오면 가지고 있었던 lock을 release하고 OS가 반환된 것을 다음 thread에게. 한

thread가 끝난 다음에 다음 thread 실행하도록 해서 consistent한 결과를 내게 한다. 

 

Creation / initialization

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • pthread_mutex_t
     mutex를 사용을 하려면 mutex에 대한 lock을 요청하기 전에 먼저 mutex type의 변수를 선언하고 초기화를 진행해야 한다. 보통은 mutex 타입이 구조체 형태로 구현이 된다. mutex lock을 표현할 수 있는 변수가 되는 것이다. 
  • Initialization of static variable (초기화 하는 방식이 2가지가 제공이 된다)
    static으로 변수를 선언했을 경우에 선언과 동시에 초기화 할 수 있는 default 방식으로 초기화 할 수 있는 
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 방법. 이 방법은 주로 static 변수로 선언된 mutex type의 변수를 선언과 동시에 초기화 하기 위한 방법이다. 
  • Initialization of dynamically allocated variable
    mutex type의 변수를 dynamic allocated로 선언했을 경우 초기화 함수를 호출하는 방식이 있다.
    pthread_mutex_init이라는 함수를 호출해서 mutex 변수를 초기화하는 방식도 있다. 첫번째 파라미터가 바로 초기화 할려고 하는 mutex pointer변수의 포인터를 넘겨주고 두번째 파라미터는 mutex변수도 속성을 가지고 있고 원하는 속성으로 하고 싶을 경우 여기에 넘겨준다. 속성을 줄 수 있다는 차이점이 있다. 
  • Return values
    성공하면 0을 반환하고 실패하면 nonzero error code를 return 한다. 
  • Initialization of a mutex that has been already initialized
    이미 초기화된 mutex 변수를 다시 초기화 한다. 예를 들어 init 함수를 호출했었는데 다시 한번 호출을 한다던지. 이런 결과에 대해서는 따로 define 되지 않아서 어떤 일이 생길지 모른다. 

 

Destroy

#include <pthread.h>
int pthread_mutex_destroy(phread_mutex_t *mutex);

초기화하고 난 다음에 lock을 하고 lock을 해제할 수 있는데 이러한 작업이 끝나고 mutex 변수를 다 사용하고 난 다음에는 destroy함수를 호출해서 관련 mutex 변수에 할당됐던 resource를 해제하는 작업을 OS에게 요청해서 resource를 효율적으로 활용해야 한다. 

  • Return values
    성공적으로 destroy되면 0이 return이 되고, 에러가 나면 error code값이 return이 된다. 
  • Undefined behaviors (destory할 때도 조심해야 한다)
    - destroy 된 다음에 이 mutex 변수를 참조하는 thread가 있었는 경우. destroy 된 mutex 변수를 reference 할려고 하면 당연히 안된다. (확인하고 destroy 해라)
    - 어떤 thread가 destroy를 호출했는데 다른 thread가 mutex lock을 가지고 있는 경우. Mutex 변수를 사용하는 thread가 있는 경우.

 

Locking / unlocking

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_lock() 
    - OS에게 lock을 요청하는 함수. 함수를 호출하기 전에 이 함수를 호출해서 변수를 넘겨준다. 예를 들어서
    pthread_mutex_lock(&m); 을 호출할려면 m은 이미 초기화가 되어있다고 가정하고 호출한다. 그러면 모든 thread들은 함수를 호출하기 전에 mutex_lock을 호출하게 될것이고 여러 개의 thread가 동시에 lock을 호출하게 되면 lock한 함수를 block 시킨다. mutex가 available할 때까지. 성공적인 경우(lock을 얻은 경우)에만 return을 한다. lock을 놓으면(unlock이 되면) block에 있는것이 실행. 

  • pthread_mutex_trylock() -> nonblocking 버전으로 호출해보는 함수.
    - Always returns immediately : lock을 얻을 수 있는 체크해 본다. lock을 얻으면 ok이고 그렇지 않아도 return한다. 호출했을때 성공하면 0, lock을 얻지 못했을 경우(이미 할당이 된경우) return이 된다. lock을 얻을때까지 trylock쓰면 된다. lock을 얻지 못하면 다른 task 수행하겠다 이런 경우 사용할 수 있다. 
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mylock);
/* critical section */
pthread_mutex_unlock(&mylock);

개발자는 다중 thread 쓸때 critical section만들어야 되는 부분에서 lock와 unlock 함수를 호출하면 된다. 

 

At-Most-Once execution

#include <pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
pthread_once_t once_control = PTHREAD_ONCE_INIT;

At-Most-Once excution의 semantic을 알아야 한다.  At-Most-Once의 의미는 기껏해야 한번 실행하는 함수라는 의미이다. 예를 들어, func( )라는 함수가 있는데 한번만 실행해야 하는 함수이다. (아까 얘기한 초기화 함수같은) 개발자가 사람이다 보니 한번만 실행해야 되는 함수를 다중 스레드로 짜다 보니 또 호출하는 실수를 할 수가 있다. 그런 상황 하에서도 실제 실행되는건 젤 처음 한번만 실행되게 하기 위해서 pthread_once라는 함수를 사용한다. 

pthread_once() 가 at most once를 보장해주는 함수이다. 이 함수를 사용하려면 변수를 먼저 초기화하고 어떤 변수에 대해서 초기화하고 첫번째 파라미터로 넘기고 두번째 파라미터는 at most once로 실행해야 하는 함수를 넘겨준다. 여러 스레드에 의해 호출된다 하더라도 여기 지정하면 한번만 실행된다. 이 함수를 실행하기 위해서는 pthread_once_t 타입을 초기화해서 사용해야 한다. mutex 변수와 다르게 static하게 초기화 하는 방법만 제공이 된다. 근데 두번째 파라미터의 타입을 보면 파라미터가 없고 void 리턴타입의 함수를 실행을 시킬 수 있는 제약상황이 존재한다. 결국 mutex init함수는 pthread_once로 호출할 수가 없다(parameter가 있기 때문에) 또다른 alternative한 방법을 사용해야 한다. 

 

printinitonce.c

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

static pthread_once_t initonce = PTHREAD_ONCE_INIT;
int var;

static void initialization(void){
	var = 1;
    printf("The variable was initialized to %d\n",var);
}

int printinitonce(void)	/* call initialization at most once */
	return pthread_once(&initonce, initialization);
}

initialization함수를 한번만 실행하고 싶어서 printinitonce를 호출하게 되면 pthread_once 함수를 통해서 initialization을 간접적으로 호출한다. 여러 함수가 동시에 printinitonce를 호출해도 initialization은 한번만 실행이 되고 그 다음 스레드에 의해서 호출되도 실행이 안되도록 보장이된다. 즉, at most once execution이 보장이 된다. 

#include <stdio.h>

int printinitonce(void);
extern int var;

int main(void) {

   printinitonce();
   printf("var is %d\n",var);
   printinitonce();
   printf("var is %d\n",var);
   printinitonce();
   printf("var is %d\n",var);
   return 0;
}

printinitonce를 실행이 var=1로 초기화 되어서 출력이 되고 main 함수에 의해서 1이다 라고 표시가 되고 printinitonce를 또 호출해도 다시 실행이 되지는 않는다.(pthread_once 함수에 의해서) 

parameter가 지금은 void로 와야만 되는데 parameter가 필요한 경우에는 어떻게 할까??

 

- Alternative example : printinitmutex 함수 사용한다. 초기값을 parameter로 넘겨준다. 

 

printinitmutex.c

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

int printinitmutex(int *var, int value) {
   static int done = 0;
   static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
   int error;
   if (error = pthread_mutex_lock(&lock))
      return error;
   if (!done) {
      *var = value;
      printf("The variable was initialized to %d\n", value);
      done = 1;  // 끝냈다는 걸 보여준다. 
   }
   return pthread_mutex_unlock(&lock);
}

다시 printinitmutex를 호출해도 static으로 선언된 곳은 건너뛴다. done은 1로 이미 설정되어버려서 if문도 건너뛴다. if문 안에는 최초일때만 실행된다. 다시 실행이 되지 않는다. done 변수도 초기화가 한번만 되고 mutex변수도 lock이라는 변수가 또 초기화 되는것이 아니라 static변수라서 처음 한번만 초기화 되니까 조건이 만족하게 되는 것이다. 

#include <stdio.h>

int printinitmutex(int *var, int value);

static void print_once_test(int *var) {
   int error;

   error = printinitmutex(var,1);
   if (error)
      printf("Error initializing variable\n");
   else
      printf("OK\n");
}

int main(void) {
   int var;

   print_once_test(&var);
   print_once_test(&var);
   print_once_test(&var);
   return 0;
}

static 변수의 초기화를 이용해서 한번만 실행되도록 구현을 한것이다. 

 

At-Least-Once execution

: 적어도 한번 실행해야 한다. 한번 실행은 반드시 하고, 두 번 실행이 되어도 상관없다. 초기화를 반드시 해야 한다. At least Once 와 At most once의 교집합은 exactly once이다. 한번도 실행을 안해도 안되고 여러번 해도 안되고 정확히 한번만 해야 된다. 

 

Conditino Variables : 또다른 동기화 mechanism

Mutex lock에 의한 동기화는 critical section을 구현하기 위한 동기화 mechanism으로 lock()과 unlock()이 있었다. 여러 thread가 달려들더라도 한 thread만 진입할 수 있도록 동기화가 된 것이다. locking mechanism을 사용하다 보니 또다른 동기화 요구가 생겼다. 

Condition Variable을 사용하는것은 critical section 안에서 또다른 동기화가 필요할 때 사용하게 된다. Condition Variable은 critical section 내에서 사용하는 것이다. 

Motivation

- 어떤 thread가 Mutex Lock을 써서 동기화를 써서 critical section에 들어왔다. 들어와서 어떤 task를 처리하려고 봤더니 지금 이 task는 특정조건을 만족해야만 수행할 수 있는 task였던 것이다. 그런데 지금 이 thread가 critical section안에 들어와서 봤더니 다음 task를 수행할 조건이 만족하지 않고 있는 상태였던 것이다. 그럼 thread는 다음 task를 수행하지 못하고 기다려야 하는 상황이 생기는 것이다. 

 예를 들어 조건중에 x라는 변수와 y라는 변수가 있는데 critical section에 들어온 thread가 다음에 수행할 task는 일단 x와 y가 같다라는 조건 하에서 수행해야 하는 task였던 것이다. 그런데 봤더니 x와 y가 값이 다른 것이다. 그러면 이 thread는 x와 y가 같아질때까지 기다려야 하는데 while(x!=y)를 써서 기다리면 의미없는 기다림(Busy waiting)이 된다. 근데 busy waiting은 굉장히 비효율적이다. 그냥 lock을 놓고 나갈 수는 없다. 그러면 별도의 또다른 wait를 할 수 있는 mechanism이 필요하다. 

- Non-busy waiting solution : 여기서 나오게 된 것이 condition variable 즉, 조건 변수라는 mechanism이 필요하다. 일단 lock을 얻고 access 하자. x==y인 경우 unlock하고 loop를 빠져나가면 되는데, 만약 false인 경우, thread를 suspend 시키고 unlock한다. (thread를 suspend시키는 또다른 함수) 그냥 suspend 시키면 안되고 무조건 mutex를 unlock하고 suspend를 해야 한다. critical section안에서 대기해야 된다. 대기를 하기 위해서 condition variable이라 하는것에 대해서 wait를 한다. condition variable에 대해서도 waiting queue가 존재한다. suspend되는 것은 어딘가 waiting queue에 들어가서 대기하게 되는 것이다. Critical section은 잠시동안 비어있는 상태가 되는 것이다. 그러면 다른 thread가 들어올 수 있는 기회를 줘야된다. 그러니까 반드시 suspend시킬 때 내가 가진 mutex를 unlock시켜줘야 한다. 이제 다른 thread가 들어와서 waiting하고 있는 x와 y를 같게 해주는 thread가 존재하는 것이다. 아까 x와 y값이 달랐기 때문에 대기하고 있었던 thread는 깨워진다. 어떤 제어상황안에서 condition variable에 의해서 제어가 되는 것이다. 혹시나 x값이 변경이 되었으니까 check를 하도록 기다릴 수 있는 thread를 깨워줄 수 있는 thread를 호출 한다. 같은 condition variable에 대해서 signal이라는 함수를 호출하면 이 signal에 의해서 깨우게 된다. 깨워진 thread를 다시 검사를 하고 여전히 다르면 또 wait로 호출하고 대기한다. 이렇게 서로 다른 thread가 하나의 critical section인 것이다. 한 thread는 조건 검사를 하고 다른 critical section에 들어와서 깨워주고. 이러면 조건에 맞으면 조건 검사를 하고 조건이 맞으면 다음 section을 진행. 조건을 검사하고 깨워주고 wait하는 thread를 위해서 condition variable이 필요하다. 

 

- pthread_cond_wait : condition variable이랑 mutex를 parameter로 받고 thread를 suspend시키고 mutex를 unlock하는 함수. mutexx를 가진 thread에 의해서 호출되어야 한다.

- pthread_cond_signal : condition variable을 parameter로 가지고 적어도 하나의 thread를 깨운다. condition variable에서 대기중인 thread를 mutex의 waiting queue로 이동시키는 역할을 한다. 그래서 wait에서 깨어나면 먼저 mutex를 다시 얻고 실행을 해야 한다. 

 

#include <pthread.h>
int pthread_cont_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

성공적으로 초기화가 되면 0이 반환이 되고 에러가 나면 error code가 반환된다. 

 

Destroying condition variables

#include <pthread.h>
int pthread_cond_destory(pthread_cond_t *cond);

 

Waiting condition variables

#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t * restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

pthread_cond_wait 함수 : 1.앞에서 초기화한 condition variable넘겨줌 2.unblock 시킬 mutex parameter

-> 이걸 호출하게 되면 첫번째 파라미터에 대해서 호출한 thread가 suspend 되면서, 즉 첫번째 파라미터의 waiting queue로 들어가게 된다. 내가 잠들고 난 다음에 다른 thread가 들어올 기회를 줘야하기 때문에 두번째 파라미터를 unblock 시킨다. 

timedwait도 parameter는 똑같은데 세번째 파라미터가 abstime이 들어가게 된다. 누군가가 나를 깨워주든지(signal) 아님 abstime시간이 만료되면 return이 된다. 

Signal이 도착했을 경우에는 2가지 가능성이 있다. 첫번째는 signal handler가 불릴것이고 return 하고 난다음에 wait함수가 기다리는 상태가 계속되는 경우, 또는 sinal handler로 부터 return 한 다음에 wait도 0을 return해서 깨어나는 경우도 있다. (wait함수와 signal delivery 같이 사용하는 경우)

 

Signaling on condition variables : condition variable에 대해서 대기중인 thread를 깨워주는 함수 

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

parameter는 깨울 condition variable을 유일한 파라미터로 취함.

일반적인 함수가 cond_signal함수이고 이 함수를 호출하게 되면 파라미터로 넘긴 condition variable의 waiting queue에 대기중인 thread들 중에서 적어도 하나의 thread를 깨운다. 깨운다는 얘기는 condition varaible의 waiting queue에서 꺼내서 unblock했던 mutex에 waiting queue로 다시 넣는다. condition variable에서 깨어난 thread는 일단 자기가 놨던 mutex를 먼저 다시 lock을 가진 상태에서 wait함수를 리턴할 수 있게된다. 

broadcast라는 함수는 condition variable에 대기하는 모든 thread를 깨우겠다. 무조건 모든 waiting 하는 thread를 깨우겠다. 

 

Signal handling and thread : 다중thread 프로그램에서 signal handling하기

Signal delivery in threads

- 멀티 thread에서 thread handling하는 것을 고려해보는 것이다. Thread가 여러개인것이 실행중인데 그 thread에게 signal이 도착했을 경우 어떤 일이 벌어질 것인가. Process안의 모든 thread들은 process의 signal handler들을 공유한다. 각각의 thread들은 thread들마다 signal mask를 가질 수 있다. Signal mask를 얘기할 때 사전조건이 이 process는 signle threaded process다 라고 가정하고 그 process의 signal maks를 sigprocmask라는 함수를 통해서 제어를 할 수 있었다. 만약에 다중 thread기반의 process에서 signal mask를 제어하고 싶을때는 sigprocmask말고 다른 POSIX libraray에서 제공하는 thread별로 mask를 조절할 수 있는 함수가 있다. Thread별 signal mask를 제어할 수 있는 함수는 뒤에서 설명하겠다.

다중thraed기반의 process에게 signal이 왔을 때 이 signal은 어떤 thread가 받을 것인가 하는 것이 애매하게 된다. 그래서 그건 delivery mechanism에 따라서 3가지 타입의 signal이 전달되는 방식을 생각해볼 수 있다. 

1. asynchronous : 그 signal을 unblock한(signalmask로 막지 않은) thread에게 signal이 전달되는 경우. 이 signal을 받고자 하는 thread가 여러개가 있다면 여러개의 thread에게 signal이 전달될 수 있다. 

2. Synchronous : 여러 thread들 중에서 그 signal을 발생시킨 thread에게 delivery한다. 

3. Directed : target signal을 지정했을때 thread를 target thread 목적 thread가 이미 지정되어있다.

 

Directing a signal

#include <signal.h>
#include <pthread.h>
int pthread_kill(pthread_t thread,int sig);

내가 이 signal을 전달시킬 thread를 지정한다. 첫번째 파라미터가 thread id가 된다. 두번째 파라미터로 몇번 signal을 보낼것인지. 

 

Masking signals for threads

#include <pthread.h>
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

sigprocmask의 parameter와 동일하다. 

how parameter : SET_MASK -> 기존의 signal mask에 등록되어있었던 signal들을 무시하고 새로 2번째 파라미터로 설정한 sigset으로 설정, SIG_BLOCK -> 현재 signal을 그대로 두고 2번째 파라미터 sigset 추가, SIG_UNBLOCK -> 현재 signalmask에 있는 것중, 2번쨰 파라미터 sigset을 빼는 것.

oset은 변경되기전 signal mask 저장.

 

Dedicating threads for signal handling

signal handling을 전담하는 thread를 지정하는 방식 -> signal이 오면 전담 thread 지정하는 방식으로. 이 예제를 통해서 다중 thread가 존재하는 process에서 signal handling 필요 있을때 이렇게 처리해라. 

signal handling을 처리하는 특정 thread를 지정을 해라. 어떤 식으로 만들거냐. 일단 main thread가 일단 모든 signal을 block을 시키고 dedicated할 thread 만들고 전담 thread가 signal이 도착하기 기다리는 sigwait()를 전담 thread가 호출하고 sigwait함수의 target signal을 sigset안에 기다리는 것을 넣어서 호출하면 sigwait는 와서 block이 되면 sigwait가 return이 되고 pending list에서 삭제하고 return이 된다. 모든 thread block해서 다 wait되고 전담 thread가 sigwait 밑에 작업을 수행하면 되는 것이다. 이 전담 thread는 target signal이 와서 pending이 되면 수행할 task.

 pthread_sigmask를 통해 unblock하는 방법을 사용할수도 있을거다. 

 

#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include "doneflag.h"
#include "globalerror.h"

static int signalnum = 0;

/* ARGSUSED */
static void *signalthread(void *arg) {    /* dedicated to handling signalnum */
	//signalhandling 전담 thread가 수행할 함수
   int error;
   sigset_t intmask;
   struct sched_param param;
   int policy;
   int sig;

   if (error = pthread_getschedparam(pthread_self(), &policy, &param)) {
      seterror(error);
      return NULL;
   }
   fprintf(stderr, "Signal thread entered with policy %d and priority %d\n",
              policy,  param.sched_priority);
   if ((sigemptyset(&intmask) == -1) ||
       (sigaddset(&intmask, signalnum) == -1) ||
       (sigwait(&intmask, &sig) == -1))
      seterror(errno);
   else
      seterror(setdone());
      return NULL;
}

int signalthreadinit(int signo) { //signo -> target signal
   int error;
   pthread_attr_t highprio;
   struct sched_param param;
   int policy;
   sigset_t set;
   pthread_t sighandid;

   signalnum = signo; //target signal은 전역변수로 저장    /* block the signal */
   if ((sigemptyset(&set) == -1) || (sigaddset(&set, signalnum) == -1) ||
      (sigprocmask(SIG_BLOCK, &set, NULL) == -1))
      return errno;
   if ( (error = pthread_attr_init(&highprio)) ||    /* with higher priority */
        (error = pthread_attr_getschedparam(&highprio, &param)) ||
        (error = pthread_attr_getschedpolicy(&highprio, &policy)) )
      return error;
   if (param.sched_priority < sched_get_priority_max(policy)) {
      param.sched_priority++;
      if (error = pthread_attr_setschedparam(&highprio, &param))
         return error;
   } else
     fprintf(stderr, "Warning, cannot increase priority of signal thread.\n");
   if (error = pthread_create(&sighandid, &highprio, signalthread, NULL))
      return error;
   return 0;
}

pending되면 sigwait리턴되고 아래쪽 실행하게 된다 -> setdone()함수 수행. 그 프로세스에게 작업을 모두 끝내라는 작업이다. 이 예제에서 보고자 하는 것은 다중 thread기반의 프로세스에서 signal왔을때 전담 thread를 만들어서 그 thread가 target signal을 받아들이도록 하면 되겠다. 전담 thread는 sigwait함수 호출해서 target signal올때까지 기다림. targetsignal오면 pending 되고 setdone() 수행. 별도의 sinal handler함수가 사용되지 않음. target thread가 전달되지 않는다. 

 

Readers and writers : 조금 더 세분화해서 lock을 걸 수 있는 reader and writers lock. 

이것도 동기화 mechanism 중 하나

Reader-writer problem

- mutex lock의 목적은 특정 code영역중 여러 thread가 동시에 접근하려고 하면 문제가 되기 때문에 한번에 하나의 thread만 access하도록 하기 위해서 사용했던 것. lock을 얻은 thread만 critical section으로 진입할 수 있게 된다. 

- shared resource를 access 하는 것을 세부적으로 살펴보자. thread가 resource를 access한다 하는 것은 사실 2가지 중 하나의 operation을 진행한다는 것이다. 하나는 read. 이 resource로부터 뭔가를 읽는다. 다른 하나는 write. 이 resource에 어떤 값을 새로 쓰겠다. write operation인 경우에는 여러 thread가 접근하면 당연히 충돌 문제가 생기니까 이거는 exclusive하게 하나의 thread만 할 수 있게 한다.

 그런데 read operation인 경우 달라진다. 만약에 여러 thread가 동시에 읽어가겠다라고 하면 사실 크게 문제가 될 것이 없다. 그래서 이 경우에는 critical section으로 막을 필요가 없다. (오히려 막는게 performance가 떨어질 수도 있다) operation에 따라서 lock을 주는 그런 mechanism을 reader-write mechanism이라고 한다. 조금 더 mutex lock보다 flexible하게 lock을 제공하는 mechanism. read 용으로 lock을 요청했을 경우 여러 thread에게 lock을 줄 수있는 mechanism. 요청하는 lock이 여러개가 있다보니 섞여있는 경우 누구에게 먼저 lock을 줄 것인가 하는 strategy가 생길 수 있다. -> Strong reader synchronization이나 strong writer synchronization이 생긴다. 만약에 섞여있으면 writer operation thread가 끼어드는 순간 thread들 중에 하나의 thread들만 진행할 수 있다.(Mutex Lock과 같음) 모든 thread들이 read요청했으면 mechanism이 모든 thread에게 권한을 준다. 그럴때 read용 lock을 먼저 할것이냐 write용 lock을 먼저할것이냐에따라 동기화 진행될 것이다. 

reader synchronization은 reader에게 우선권을 주겠다는 소리이다. Reader에게 먼저 읽어라 라는 lock을 준다. Reader thread가 다 읽고 난 다음에 writer thread에게 write해라 라는 뜻. writer synchronization은 그 반대. 구현에 따라서 어떤 방식일지는 달라지는 것이다. 

 

Initialization of read-write locks

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

반드시 초기화를 하고 난 다음에 사용해야 한다는 것은 동일하다. 

pthread_rwlock_t가 read write lock 타입의 변수가 되는 것이다. 그 변수를 선언하고 rwlock 타입의 변수도 항상 초기화를 하고 사용해야 한다. 초기화 함수를 통해서 초기화 할 수 있다. 다른 pthread 함수와 동일하다. 또 초기화를 하게 되면 결과가 undefined되게 되어있다. read write lock에도 한번만 초기화 하라는 내용이 되어있는 것이다.

 

Destroying read-write locks

#include <pthread.h>
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);

다 사용한 read/write 객체는 destroy함수를 호출해서 시스템에 사용했던 resource를 반환한다. 이것도 주의해서 사용해야 한다. read /write lock을 잘못 destroy한 경우 결과는 undefined. 

 

Locking / unlocking

#include <ptread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

lock을 걸때 read 용으로 걸것인지, write용으로 걸것이지가 존재하고 lock을 요청하는 함수도 blocking 모드 혹은 unblocking 모드로 걸것인지 다양하게 존재한다. read/write lock을 얻지 못한 thread는 waiting queue에 들어가서 대기하게 된다. 처음 2개 rdlock, tryrdlock은 read를 요청하는 함수이다. wrlock은 write 용 lock을 요청하는 함수이다. unlock함수는 하나 있다. 

read/write lock도 dead lock이 발생하지 않게 조심해야 한다. lock을 가지고 있는데 또 lock을 요청하면 deadlock에 빠지게 된다. 

 

read write lcok을 사용하는 예제

: list를 구현한 예제. 일반적 list가 아니라 key를 이용해서 list정보에 access 할려는 원래의 값만 참조를 할 수 있게끔. list의 값에 access하는 함수를 구현한 예제이다. 다중 thread 공유변수의 값을 읽어가는 함수. read용 operation 또는 write용 operation을 사용해서 다중 thread가 access해도 안전한 list를 구현한 예제가 될 것이다. 

#include <errno.h>
#include <pthread.h>

static pthread_rwlock_t listlock;
static int lockiniterror = 0;
static pthread_once_t lockisinitialized = PTHREAD_ONCE_INIT;

static void ilock(void) {
   lockiniterror = pthread_rwlock_init(&listlock, NULL);
}

int initialize_r(void) {    /* must be called at least once before using list */
   if (pthread_once(&lockisinitialized, ilock))
      lockiniterror = EINVAL;
   return lockiniterror;
}

int accessdata_r(void) {               /* get a nonnegative key if successful */
   int error;
   int errorkey = 0;
   int key;
   if (error = pthread_rwlock_wrlock(&listlock)) {  /* no write lock, give up */
      errno = error;
      return -1;
   }
   key = accessdata();
   if (key == -1) {
      errorkey = errno;
      pthread_rwlock_unlock(&listlock);
        errno = errorkey;
      return -1;
   }
   if (error = pthread_rwlock_unlock(&listlock)) {
      errno = error;
      return -1;
   }
   return key;
}

int adddata_r(data_t data) {          /* allocate a node on list to hold data */
   int error;
   if (error = pthread_rwlock_wrlock(&listlock)) { /* no writer lock, give up */
      errno = error;
      return -1;
   }
   if (adddata(data) == -1) {
      error = errno;
      pthread_rwlock_unlock(&listlock);
      errno = error;
      return -1;
   }
   if (error = pthread_rwlock_unlock(&listlock)) {
      errno = error;
      return -1;
   }
   return 0;
}

여러 thread가 getdata_r 호출하면 여러 thread가 동시에 read 호출할 수 있게 한다. 모든 thread가 getdata_r만 호출했다면 동시에 getdata 호출해서 access 할 수 있게 된다. 선택적으로 read, write lock을 할 수 있게 된다. read, write를 수행하는거냐에 따라서 구현한 것이다. 

initialize_r()은 read,write를 초기화 시키는 것. mutex lock으로도 구현을 했던 것이 있었는데 read lock과 비교를 한다면 read write lock은 overhead가 있을 수 있다. read용이냐 write용 lock이냐에 따라 부가적인 처리가 필요해 overhead가 필요하다. 

 

A sterror_r implementation

error message를 호출하는 함수인데 원래 thread safe하지 않은데 thread safe하게 만들어보자.

sterror()은 thread safe하지 않은 함수였고 concurrent 하게 호출하면 문제가 된다. 이 문제를 막으려면 mutex lock으로 보호할 수 있고 perror()도 thread safe 하지 않은데 여러 thread가 호출할 수 있는 함수로 만들 수 있다. thread safe하지 않고 async-signal safe하지도 않다. async signal safe하게 만들기 위해서 signal을 잠시 block 시킬 것이다. signal이 오더라도 pending 하기 위해서 sigprocmask를 사용한다. 

#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>

static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

int strerror_r(int errnum, char *strerrbuf, size_t buflen) {
   char *buf;
   int error1;
   int error2;
   int error3;
   sigset_t maskblock;
   sigset_t maskold;

   if ((sigfillset(&maskblock)== -1) ||
       (sigprocmask(SIG_SETMASK, &maskblock, &maskold) == -1))
      return errno;
   if (error1 = pthread_mutex_lock(&lock)) {
      (void)sigprocmask(SIG_SETMASK, &maskold, NULL);
      return error1;
   }
   buf = strerror(errnum);
   if (strlen(buf) >= buflen)
      error1 = ERANGE;
   else
      (void *)strcpy(strerrbuf, buf);
   error2 = pthread_mutex_unlock(&lock);
      error3 = sigprocmask(SIG_SETMASK, &maskold, NULL);
   return error1 ? error1 : (error2 ? error2 : error3);
}

int perror_r(const char *s) {
   int error1;
   int error2;
   sigset_t maskblock;
   sigset_t maskold;

   if ((sigfillset(&maskblock) == -1) ||
       (sigprocmask(SIG_SETMASK, &maskblock, &maskold) == -1))
      return errno;
   if (error1 = pthread_mutex_lock(&lock)) {
      (void)sigprocmask(SIG_SETMASK, &maskold, NULL);
      return error1;
   }
   perror(s);
   error1 = pthread_mutex_unlock(&lock);
   error2 = sigprocmask(SIG_SETMASK, &maskold, NULL);
   return error1 ? error1 : error2;
}

mutex 변수 static하게 선언. sterror_r 함수에서는 결과적으로 original 함수 sterror를 호출하는 게 목적인데 호출하는 thread가 여러개가 있더라도 한번에 하나만 할 수 있도록 pthread_mutex_lock을 해서 호출할 수 있도록 했고 나가기 전에 unlock을 했고 이 사이를 thread safe하게 만든것이다. sigfillset에 모든 maskblcok을 해서 sigprocmask를 호출했다. 현재 process에 signalmask안에 모든 thread를 채운것이다. 

perror_r 도 똑같은 idea로 sigprocmask를 써서 asyncsignal safe함수로 만든것이다.

 

Deadlocks

: 동기화 관련 mechanism을 잘 사용하지 못하면 deadlock에 빠질 수 있다. -> 개발자에게 책임을 넘김. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

728x90
반응형

'CS > 시스템 프로그래밍' 카테고리의 다른 글

Critical sections and Semaphores  (0) 2021.12.10
Signals  (0) 2021.12.07
POSIX Threads  (0) 2021.11.23
Times and Timers  (0) 2021.11.16
UNIX Special Files  (0) 2021.11.03

+ Recent posts