pthread

OS/리눅스 & 유닉스 2012. 5. 21. 20:07

스레드는 컴퓨터 프로그래밍에서 프로세스 내부의 작업 수행 단위를 말한다. 실제로 스레드란 개념을 설명하는 것은 쉽지 않지만 개념적으로 간단히 살펴보자. 프로그래밍에서 작업 단위는 보통 프로세스를 기본으로 삼는다. 각각의 프로세스는 보통 서로 다른 프로세스에 관여하지 않는다. 이는 일반적으로 별로 문제가 되지 않는 부분이며 어떻게 보면 상당히 잘 설계되어진 모델이다. 실제로 사용자가 스케줄링을 하기보다는 커널 레벨에서 스케줄링 되어 쓰인다. 하지만 멀티 프로세스 작업 중에는 프로세스 사이의 통신 문제와 프로세스가 스케줄링 될 때 사용중인 메모리의 간섭, 문맥교환시의 시간 지연 등 여러 문제가 생긴다.
쉽게 말해 스레드는 프로세스 안에서 작업을 더 세분화하여, 스케줄링 된 프로세스 시간을 좀 더 효율적으로 사용하도록 한다. 물론 같은 프로세스 안에서의 작업이므로 문맥 교환이나 스레드 사이의 통신문제는 보다 쉽게 이루어 질 수 있다.
스레드라는 것은 앞서 언급했듯 다중 환경에서 보다 효율적인 서비스를 제공하기 위한 메커니즘으로 커널 레벨에서 수행되고 있다. 하지만 스레드를 쓰려면 동기화 작업이 필요하다. 동기화가 이루어지지 않은 스레드화는 데이터 유지의 보장성을 책임지지 않기 때문에 잘못된 데이터로 인한 시스템의 치명적인 오류가 발생할 수 있다.

스레드의 구성 요소
스레드의 구성 요소로 스레드 함수를 수행하기 위한 생성자와 소멸자, 스레드 사이에 공유되는 데이터의 동기화를 위해 Mutex라는 락(Lock)을 걸기 위한 전역 변수가 있다. 그리고 데이터의 변경을 위해 선행되어야만 하는 조건으로 조건 변수(Condition Variable)가 존재한다.
스레드를 쓰려면 먼저 스레드를 생성시키고 스레드간에 공유되는 데이터가 있는 경우에는 반드시 스레드로 수행되는 함수의 선행부에 Mutex를 초기화해야 한다. 값의 앞부분에는 락을 걸어야 하고 데이터의 값을 변경한 후 다른 스레드가 쓸 수 있도록 락을 풀어야 한다.
마지막으로 스레드간에 공유되는 데이터 중 선행 조건이 만족된 이후에만 데이터의 변경이 이루어지도록 하기 위해 조건 변수를 이용한다. 조건 변수의 사용법은 Mutex와 조건 변수를 함께 초기화해야 하고 데이터를 변경하기전에 Mutex에 락을 건 후 조건 변수가 완료되기를 기다린다. 조건이 완료되면 데이터를 변경하고 Mutex의 락을 풀어 다른 스레드의 사용을 허가한다.

스레드의 생성과 소멸
모든 프로세스는 기본적으로 하나의 스레드와 같다. 예를 들어 main으로 시작되는 함수는 그 자체로 스레드가 되어 종료와 함께 스레드의 소멸로 이어진다. 하지만 특별히 기본 스레드에서 다른 스레드를 생성하려면 스레드 생성자를 호출해야 하고 이것들의 사용이 끝났으면 소멸자를 호출하여 메모리에서 제거해야 한다.


스레드 생성
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start)(void *), void *arg)
① 위의 함수는 커널 레벨에서 수행되는 스레드를 생성한다.
② 함수의 호출 시 성공이면 리턴 값으로 0 이 아닌 값을, 실패했다면 0을 되돌린다.
③ 첫번째 인수인 스레드는 스레드 생성이 성공한 이후에 되돌아오는 스레드의 인식자이며 이 인식자의 형은 pthread형으로 unsigned int형과 동일한 유사한 형태로 제공된다.
④ 두 번째 인수인 attr은 생성할 스레드의 속성을 결정하는 인수로 보통 NULL값을 사용한다.
⑤ 세 번째 인수인 start는 스레드로 수행될 함수의 이름을 기술하는 곳으로 반드시 void형이며 인수 역시 void형 1개만을 가질 수 있다.
⑥ 마지막 인수는 함수에 들어가게 될 void형 인수를 기술하는 곳이다.

스레드 소멸
int pthread_exit(void *value_ptr)
int pthread_detach(pthread_t thread)
int pthread_join(pthread_t thread, void **value_ptr)
① 프로세스가 종료할 때 스레드는 그것의 상태와 모든 것을 완전히 종료하게 한다(이 경우 종료 이유를 기대할 수 없다).
② pthread_detach는 스레드 종료 후 메모리에 남아 있는 정보를 제거하는 역할을 수행한다.
③ pthread_join은 자식 스레드의 끝나는 상태를 기다라는 역할을 수행함으로 부모 스레드는 자식 스레드가 종료될 때까지 블러킹된다.

스레드의 주기
스레드는 Ready, Blocked, Running, Terminated의 4가지 상태를 그림 1과 같이 순회하며 그 주기를 마치게 된다.
그림 1에 나타난 것과 같이 스레드가 처음 만들어졌을 경우(create) 스레드는 Ready 상태가 된다. 그리고 커널에 의해 스케줄링 되었을 경우 스레드의 상태는 Running 상태에 들어갈 수 있다. 그리고 이 스레드에 할당된 시간이 모두 끝났다면 스레드의 상태는 다시 Ready로 돌아가고 이 스레드가 임의의 자원을 사용하려고 할 때는 그 자원을 사용할 수 있기 전까지 Blocked된다. 그리고 자원을 사용할 수 있게 되면 다시 Ready 상태로 들어가게 된다. 만일 수행중인 작업이 모두 끝났거나 사용자에 의해 강제로 취소되었을 경우 스레드는 Termina ted상태가 되어 자신의 수행중인 과정을 모두 끝내게 된다. 이때 이미 위에서 언급했지만 종료 시에 모든 자원을 소멸시키지 않으므로 사용자가 자원의 소멸에 신경 써야 한다.

스레드의 동기화
스레드를 사용함에 있어 가장 주의해야 할 조건을 동기화다. 이것은 모든 스레드가 동시에 수행되므로 임의의 데이터를 동시에 접근하여 변경시킬 수 있음을 의미하며 전역 변수나 메모리의 할당 등의 경우 동기화를 반드시 고려해야 한다.

Mutex의 생성과 파괴
Mutex는 앞서 언급했듯 스레드간의 공유되는 데이터의 변경에 앞서서 수행 도중 다른 스레드로부터 이 데이터가 변경되지 않는다는 것을 보장하기 위한 것으로 사용하기 전에 초기화(create)해야 하고 모든 사용이 끝난 후 반드시 메모리에서 제거해야 한다.

① int pthread_mutex_init(pthread_mutex_t *mutex, pthread _mutexattr_t *attr)
  Mutex를 사용함에 있어 처음 생성시키는 역할을 담당하며 첫번째 인수로 사용할 Mutex와 두 번째 인수로 사용하게 될 Mutex의 속성을 결정한다.
② int pthread_mutex_destroy(pthread_mutex_t *mutex)
  Mutex사용이 모두 끝났을 경우 Mutex의 자료구조를 다시 커널로 반환 시켜 더 이상 사용하지 않음을 명문화 시키는 역할을 한다.

Locking과 Unlocking.
스레드 사이의 공유되는 데이터를 변경하고자 할 경우 변경하는 부분의 선두에 Mutex에 락을 걸고 변경이 완료되면 다시 언락 해야 한다.

① int pthread_mutex_lock(pthread_mutex_t *mutex)
  데이터를 변경하기 전 다른 스레드에게 이 데이터를 변경하지 못하도록 Mutex에 락을 건다. 만일 다른 스레드가 이미 그 데이터를 사용하려고 Mutex에 락을 걸었을 경우 스레드가 호출한 이 함수는 실패하게 된다.
② int pthread_mutex_trylock(pthread_mutex_t *mutex)
  pthread_mutex_lock 함수와는 달리 이미 다른 스레드에서 이 Mutex에 락을 걸어 데이터를 쓰려고 하는지 조사한다. 즉 다른 스레드에서 이 Mutex에 락을 걸었다면 그에 대한 에러 코드를 돌려준다.
③ int pthread_mutex_unlock(pthead_mutex_t *mutex)
  변경하려는 데이터의 사용을 마쳤을 경우 다른 스레드에서 그 데이터의 변경을 허가하는 역할을 한다. 변경하려는 데이터의 이후에 기술되어야 한다. 만일 이미 Mutex의 언락이 이루어진 후 다시 언락하려는 함수는 실패하게 된다.

조건 변수(Condition variable)
조건 변수는 스레드간의 공유하려는 데이터의 통신 정보를 위해 사용한다. 데이터의 변경을 하기 전에 Mutex를 락 해야하고 선행 조건을 기다리게 되며 선행 조건이 완료된 이후에 데이터의 변경을 해야 한다. 선행 조건의 완료는 단일 대기 스레드에게만 신호를 주는 Signal이라는 방법과 전체 대기 스레드에게 모두 신호를 주는 brocasting 방법이 있다.
그림 2는 3개의 스레드가 하나의 데이터를 사용함에 있어 락과 언락, 그리고 조건 변수를 사용하여 데이터의 신용도를 보장하는 것을 보여주고 있다.

① 스레드 1이 데이터를 사용하고자 하여 처음 Mutex에 락을 걸고 Mutex3의 조건이 만족되기를 기다린다.
② 스레드 2가 데이터를 사용하려고 Mutex를 조사할 경우 이미 스레드 1이 사용하고 있으므로 스레드 1의 사용이 끝날 때까지 기다린다.
③ 스레드 3의 조건 변수는 스레드 1에 의해 참조되는 것으로 스레드 3의 조건이 만족된 이후에만 스레드 1의 데이터 변경이 허가된다.
④ 스레드 3의 조건 변수가 만족되고 스레드 1의 데이터의 변경이 완료된 이후 스레드 1은 이 데이터가 다른 스레드들에 의해 사용될 수 있도록 하기 위해 Mutex를 언락시킨다.

조건 변수의 초기화와 소멸
조건 변수는 사용하기 전에 반드시 초기화해야 하고 사용이 끝난 후에는 메모리에서 제거해야 한다.

① int pthread_cond_init(pthread_cond_t *cond, pthread_ condattr *condattr)
  조건 변수의 초기화를 위한 함수로 첫째 인수인 cond는 조건으로 사용되는 변수를, 두 번째 인수인 condattr은 조건으로 사용되는 변수의 속성을 나타낸다.
② int pthread_cond_destroy(pthread_cond_t *cond)
  조건 변수의 사용이 모두 끝났을 경우 커널에 자원을 되돌려 주기 위한 함수로 인수인 cond 변수의 해방을 뜻한다.

조건 변수의 waiting과 waking
데이터를 변경하기 전 반드시 다른 스레드에서 선행 조건이 완료되어야만 하는 경우 선행 조건의 pthread_ cond_wait를 사용하여 선행 조건의 완료를 기다리며 다른 스레드에서 선행 조건이 완료되었다는 것을 알려주는 방법으로 단일 스레드에게 알리는 signal과 모든 사용자들에게 알리는 brocast가 있다.

① int pthread_cond_wait(pthread_cond_t *cond, pthread_ mutex_t *mutex)
  데이터를 사용함에 있어 Mutex의 락을 건 스레드가 상황 변수의 조건을 기다리기 위한 함수다. 이 함수를 호출한 스레드는 조건이 만족되기 전까지 블러킹된다.
② int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, struct timespec *expiration)
  pthread_cond_wait 함수와 같은 역할을 하지만 일정 시간이 지난 후 자동으로 블러킹에서 벗어나게 되므로 조건이 만족되지 않아 무한히 기다라는 것을 피할 수 있다.
③ int pthread_cond_signal(pthread_cond_t *cond)
  cond의 조건을 기다리기 위해 pthread_cond_wait을 호출한 스레드에게 조건이 만족되었음을 알려준다.
④ int pthread_cond_broadcast(pthread_cond_t *cond)
  pthread_cond_signal과 같은 역할이지만 이 함수는 cond를 기다리는 모든 스레드에게 조건이 만족되었음을 알린다.


[출처] pthread|작성자 Sally


: