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 을 사용한다.

Thursday, June 5, 2008

Thread, reader-writer locks

mutex는 잠금/해제 두가지 상태밖에 없다. 때문에 한 thread가 읽는 작업을 수행하는 동안 다른 thread들은 읽을 수 없다. reader-writer lock은 잠금의 경우가 read lock/write lock 두 가지 상태가 있기 때문에 여러 thread가 동시에 읽을 수 있다. 사용되는 함수들은 아래와 같다.

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 0 for OK

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 0 for OK

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrwlock(pthread_rwlock_t *rwlock);
// 0 for OK

맨 위의 init/destroy 함수는 mutex를 만들때 사용하는 함수이고, rdlock, wrlock은 각각 읽기/쓰기를 lock하는 함수이다(lock될 때 까지 block되어 기다린다). tryrdlock, tryrwlock들은 non-block으로 동작하는 함수들이다.

rdlock으로 lock되어 있는 경우, shared mode로 lock된 것이고, wrlock으로 lock한 경우에는 exclusive mode로 lock한 것이다.

write lock이 된 상황에서 다른 thread들이 rwlock/wrlock 을 하려는 경우 lock이 풀릴 때까지 모두 block되는 반면, read lock이 걸린 경우에는 read lock을 거는 thread들은 바로 접근이 가능하게 되고, write lock을 시도하는 경우에는 걸려있는 모든 read lock들이 해제될 때까지 block 된다.

비동기 lock 함수들을 사용하는 경우 lock할 수 없는 경우 리턴 값으로 EBUSY에 해당하는 값을 반환한다.

Socket, getsockname

이미 binding 된 socket으로 부터 주소정보를 받아올 때 getsockname을 사용한다. 아래와 같이 사용하면 된다.

getsockname(sfd, (struct sockaddr *)&saddr, &len_saddr);

Wednesday, June 4, 2008

Socket, thundering herd problem

network 프로그래밍시 주의해야할 문제 중에 하나로 thundering herd problem이 있다. 천둥이 치면 소떼가 뛴다...는 상황을 비교해서 쓰는 말인데, 여러 thread/process 간에 공유된 자원이 있을 때 자원이 사용가능하게 되지마자 block되어 있던 모든 프로세스들이 깨어나지만 한 프로세스만 자원을 점유하고 나머지는 다시 block되어야 하므로 CPU time을 낭비하게 된다. Mutex 같은 lock을 통해 이런 문제를 해결할 수 있다고 하는데,.. 그런가 ㅡ.ㅡ;,..

Monday, June 2, 2008

Socket, code example

간단히 ... 잊지 않을 정도의 코드 예제를 적어본다.
server side

#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

int sfd, rsfd;
struct sockaddr saddr; // listener
struct sockaddr rsaddr;

sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

saddr.sin_family = AF_INET;
saddr.sin_port = 10000;
saddr.sin_addr.s_addr = inet_addr("100.100.100.100");
// or inet_aton

bind(sfd, (struct sockaddr*)&saddr, sizeof(saddr));

listen(sfd, BACKLOG);

rsfd = accept(sfd, (struct sockaddr *)&saddr, &len_rsaddr);

recv(rsfd, rbuf, sizeof(rbuf), 0);

send(rsfd, sbuf, sizeof(sbuf), 0);

shutdown(rsfd, SHUT_RDWR); // or close

client side

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int sfd;
struct sockaddr saddr;

sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

saddr.sin_family = AF_INET;
saddr.sin_port = 10000;
inet_aton("100.100.100.100", &saddr.sin_addr);
// or inet_addr

connect(sfd, (struct sockaddr*)saddr, sizeof(saddr));

send(sfd, sbuf, sizeof(sbuf), 0);

recv(sfd, rbuf, sizeof(rbuf), 0);

close(sfd); // or shutdown

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