Showing posts with label thread. Show all posts
Showing posts with label thread. Show all posts

Friday, June 6, 2008

Threads, condition variables

Thread를 동기화하는 다른 방법 중의 하나로 Condition variable을 사용하는 것이 있다. condition variable을 잘 사용하면 race-free한 thread 코드를 만들 수 있다.

condition variable자체는 mutex 에 의해 보호된다. 때문에 condition 상태를 바꾸기 위해서는 먼저 mutex lock을 걸어줘야한다. condition variable(이하 cv) 초기화는 두가지 방법이 있는데, cv가 static이면 PTHREAD_COND_INITIALIZER를 넣어주면 되고 dynamic 이면(동적 할당되었으면) pthread_conf_init()을 쓰면된다. 사용이 끝나면 pthread_cond_destroy()를 호출해준다.

#include <pthread.h>
// 0 for OK
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);

cv를 체크하는 방법으로는 아래의 두 함수를 사용할 수 있다. 아래 wait함수를 호출할 때는 인자로 넣어주는 mutex가 lock된 상태여야 한다. 그러면 wait 함수 내부에서 cv를 기다리는 threads list에 함수를 호출한 thread를 넣고 mutex를 unlock한 후 wait상태에 들어가게 된다. wait하다가 condition 이 true가 되면 mutex를 lock한 상태로 리턴하게 된다. timedwait함수의 경우는 얼만큼 오래 기다릴지를 timespec으로 지정해줄 수 있다. timedwait의 경우 condition이 발생하지 않았는데 expire된 경우 lock을 한 상태에서 ETIMEOUT error를 반환한다.

#include <pthread.h>
// 0 for OK
int pthread_cond_wait(pthread_cont_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict timeout);

wait 함수들은 condition이 발생했다는 것은 알려주지만 자신이 이를 처음으로 통보받았다고 보장하지는 않는다. (예를 들어 한개의 condition이 있고 4개의 thread 가 있다면 각각의 thread가 pthread_cond_wait()하다가 condition이 발생해서 pthread_cond_wait()을 빠져 나왔다면, condition이 발생한 것은 확실하지만, 제일 처음 이를 통보받은 thread가 condition과 관련된 부분을 변경했을 수도 있다는 것이다) 때문에 이를 위해 condition을 다시 체크해보아야 한다.

이번에는 반대로 condition을 변경해주는 부분을 생각해보자. 아래 함수들을 사용해서 두가지 방법으로 condition이 변경되었음을 알려줄 수 있는데, signal 은 기다리고 있는 thread중 하나만 wakeup시켜주고 broadcast는 모든 thread를 깨워준다.

#include <pthread.h>
// 0 for OK
int pthread_cond_sigal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

아래 APUE 에서의 간단한 예제를 살펴보자.

#include <pthread.h>

// queue의 노드
struct msg {
struct msg *next;
/* ... */
}

struct msg *workq;
// condition variable
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
// mutex to protect c.v.
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void process_msg(void)
{
struct msg *mp;
while (1) {
pthread_mutex_lock(&qlock);
// 이미 lock했어도 아래 ...wait 에서
// condition variable을 등록 후 내부
// 적으로 unlock하기 때문에 block상
// 태가 된다.
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
// wait 에서 condition 이 바뀐 것을
// 통보 받고 lock된 상태로 빠져 나왔음
// 아래 enqueue_msg 에서 broadcast로
// 통보 했다면 여기서 queue가 비었는지
// 다시체크해야하나, 그렇게 할 경우
// thundering herd가 발생할 소지가 있음

// 원래 queue는 tail에서 값을 뽑아야 하는데,
// 여기서는 head에 넣었다가 head 에서 뽑는다
// (FIFO가 아니 LIFO,..stack이다..)
// 이유는... 글쎄 ㅡ.ㅡ;..
mp = workq;
workq = mp->next;
pthread_mutex_unlock(&qlock);
// processing msg here
}
}

void enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}

Thread, manager worker model


Worker manager model 을 구현할 때 thread를 사용할 수 있다. 외부로 부터 요청된 job들을 관리하기 위해 manager는 일반적으로 queue를 사용한다. job 이 들어오면 어떤 thread가 처리해야할 지 판단한 후에 thread id와 함께 해당 job을 queue에 넣는다. 그럼 각각의 thread들이 queue를 보면서 자신에서 어떤 job이 assign되었는지 확인하고, 꺼내어 작업을 처리할 수 있다.
따라서 worker 나 manager 가 접근할 때 queue 가 lock 되어있어야 하는데, 이럴 때 mutex보다는 읽을 때는 shared mode 로 lock 할 수 있는 rwlock 을 사용한다.

Monday, June 2, 2008

Thread, mutex - deadlock/race condition avoidance

여러개의 thread가 같은 메모리에 접근할 때 발생할 수 있는 문제가 deadlock, race condition이다. deadlock을 만드는 가장 간단한 방법은 같은 mutex에 두번 lock을 거는 것이다. 혹은 두 thread A,B가 있고 mutex X,Y가 있을 때 A가 X를 lock하고 Y자원을 사용하고자 기다릴때 B가 Y를 lock하고 X자원을 사용하고자하는 경우 이런 상황이 발생한다. thread에서 이런 상황을 피하는 방법으로는 아래의 두가지가 있다.

  • 여러 mutex를 사용하는 경우 lock하는 순서를 항상 동일하게 한다. (위의 예라면 항상 X를 lock한 후 Y를 lock하는 식으로 정해서 deadlock을 피할 수 있다)

  • 자신이 사용하고자 하는 자원이 다른 thread에 의해 사용중인 경우 자신이 점유하고 있는 자원을 해제한 후 기다린다.


linux에서 사용가능한 mutex관련 함수로는 아래 것들이 있다.

#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);

mutex를 사용하기 위해서는 ..._init(), ..._destroy() 함수들을 사용해야한다. ..._init()함수 사용시 mutex의 종류를 정해줄 수 있는데, 아래와 같은 매크로로 지정할 수 있다.

  • PTHREAD_MUTEX_NORMAL : linux에서는 "timed" mutex이다.

  • PTHREAD_MUTEX_RECURSIVE : 여러번 잠글 수 있는 mutex

  • PTHREAD_MUTEX_ERRORCHECK : 에러 체크해주는 mutex

  • PTHREAD_MUTEX_MUTEX_DEFAULT : linux에서는 첫번째의 ..._NORMAL과 같다.


위에서 설명한 "timed mutex"는 기본 mutex인데, 중복으로 lock을 걸면 deadlock에 걸리고, 다른 thread가 lock한 상태에서 내가 풀려고 하면 undefined 상태가 된다. errorcheck 타입은 데드락의 경우 에러를 리턴해준다. 그 외에 adaptive mutex가 있는데, 표준은 아니고, 플랫폼에 따라 최적의 매커니즘으로 동작하게 된다. linux에서 SMP가 지원되는 경우 spinlock을 사용하게 되는데, 이때 adaptive mutex를 사용하게 되면 짧은 시간동안의 lock, unlock에 대해 최적의 성능을 지원한다. 대신 비 표준이라 PTHREAD_MUTEX_ADAPTIVE_NP를 통해 초기화 한다.

이런 mutex 타입을 mutex attribute 함수들을 통해 설정하여 mutex init시 사용하면 된다.

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutex_gettype(pthread_mutexattr_t *restrict attr);
int pthread_mutex_settype(pthread_mutexattr_t *attr, int type);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

Thursday, May 29, 2008

Thread, basic II

thread에 사용되는 함수들은 아래와 같다.

  • pthread_create()
    thread를 만든다.

  • pthread_exit()
    종료할때 return 대신 쓰면 cleanup함수들이 실행된다.

  • pthread_join()
    다른 thread가 종료될 때까지 현재 코드를 block한다. 종료될 때 그 thread의 리턴값을 받을 수 있다.

  • pthread_cancel()
    다른 thread에게 종료 신호를 보낸다. 종료하는 건 신호를 받은 thread 맘이다. 종료하게 되면 pthread_exit()가 호출된 것처럼 동작한다.

  • pthread_cleanup_push()
    종료시 실행할 함수를 등록한다 like atexit(). 종료시 stack처럼 최근 등록된 순서대로 pop되면서 실행된다.

  • pthread_cleanup_pop()
    위의 ..._push()함수로 등록한 것을 최근 순서로 해제(pop)한다. 단 인자가 0이 아니면 실행하면서 pop한다.

Monday, May 19, 2008

Thread, Basic

LSP에서는 thread를 다루고 있지 않다. 그래서 APUE의 내용을 통해 복습해 보려고 한다. 알다시피 thread는 한 프로세스 내에서 여러개로 되어 동작하기 때문에 프로세스 내의 메모리나 fd를 공유한다. 그렇기 때문에 이러한 프로세스 내 공유 자원들에 접근할 때는 항상 consistency를 생각해야 한다. thread를 사용하는 일반적인 장점들은 아래와 같다.

  • asyncronous event를 다루는 루틴을 event type에 따라 별개의 thread를 만들어 처리하면 코드가 간단해진다.

  • 여러 프로세스들을 사용하면서 fd, memory를 공유하려면 복잡한 커널의 system call을 사용해야하는 반면에 thread에서는 간단하게 처리할 수 있다.

  • 한 thread에서 직렬로 진행되던 루틴들이 병렬화 되어 성능 향상을 가져올 수 있다.

  • Single processor에서도 성능향상이 있을 수 있다(한 thread가 block일때 다른 thread는 돌 수 있으므로)

  • multiple thread를 사용하므로써 사용자 응답시간이 좋아진다(user input/output을 thread로 돌리면 되므로) .

  • Multi-processor 환경에서 성능 향상을 가져다 준다.


한개의 thread는 여러 정보들을 가지고 있는데 아래와 같다.

  • thread ID, 레지스터 값들, 스택, 스케쥴 priority, policy, signal mask and errno


소위 pthread라 불리는 것들은 POSIX.1-2001의 optional feature이다. 이게(optional한 부분이) 지원되는지 안되는지는 _POSIX_THREADS 를 #ifdef로 테스트 해보면 된다(아님, sysconf에서 _SC_THREADS로 호출해보거나..)

thread identification pid_t 처럼 thread에서는 pthread_t 를 쓴다. 그런데, 구현에 따라 pthread_t를 구조체로 만드는 경우도 있기 때문에 tid1 == tid2 식으로 비교할 수는 없고 아래 함수를 사용한다. 자신의 tid를 보기 위해서는 pthread_self()를 사용한다.

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
pthread_t pthread_self(void);

일단 thread 만드는 함수를 알아야 할텐데, 아래와 같다.

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void), void *restrict arg);

좀 어려워 보이지만 인자부터 체크해보자, 첫번째는 tid가 반환 될 것이고 두번째는 attributes를 넘겨줄 때 쓴다. 새로 생성된 thread는 start_rtn 루틴 부터 시행을 시작할 것이고, 이 함수는 arg를 인자로 받는다. thread는 signal mask를 상속받지만, pending 되어 있던 signal들은 clear된다. 조심할 점은 thread가 만들어지면 어떤 것이 먼저 실행될지 알 수 없다는 것이다. 대충 알았으니 한번 만들어보자.

#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

pthread_t ntid;

void * thr_func(void *arg)
{
printf("new thread id(%u)pid(%u)\n",
(unsigned int)pthread_self(), (unsigned int)getpid());

return (void *)0;
}

int main()
{
int err;

err = pthread_create(&ntid, NULL, thr_func, NULL);
if (err != 0)
fprintf(stderr, "pthread_create error\n");

printf("main thread id(%u)pid(%u)\n",
(unsigned int)pthread_self(), (unsigned int)getpid());
sleep(1);
return 0;
}

linux에서는 pid가 다른 경우도 있고, tid는 4자리로 나타나는 것으로 되어 있는데, 실제 돌려보니 pid가 같고, tid도 연관성이 없는 것처럼 보인다(FreeBSD처럼 나온다) 왜일까. pthread_self 대신 gettid()를 사용하면 pid와 비슷하게 생긴 tid를 얻을 수 있다.