이전 섹션에서 논의한 동기화 방법은 커널 내의 동기화와 관련되므로 커널 개발자에게만 제공된다. 반면에, POSIX API는 사용자 레벨에서 프로그래머가 사용가능하고, 특정 운영 체제 커널의 일부가 아니다.
이 섹션에서는, Pthreads와 POSIX API에서 사용할 수 있는 뮤텍스 락, 세마포어, 조건 변수를 다룬다. 이러한 API는 UNIX, Linux, macOS 시스템의 개발자가 스레드를 생성하고 동기화하는 데 널리 사용된다.
뮤텍스 락은 Pthreds에서 사용되는 기본적인 동기화 기술을 나타낸다. 뮤텍스 락은 코드의 critical section(임계 구역)을 보호하는 데 사용되는데, 스레드가 임계 구역에 들어가기 전에 lock을 획득하고, 종료할 때 lock을 해제한다. Pthreads는 뮤텍스 락에 pthread_mutex_t 데이터 타입을 사용한다. 뮤텍스는 pthread_mutex_init() 함수로 생성된다.
첫 번째 매개변수는 뮤텍스에 대한 포인터이고, 두 번째 매개변수로 NULL을 전달하면 뮤텍스의 기본 속성으로 초기화한다. 이는 아래에 설명되어 있다:
#include <pthread.h>
pthread_mutex_t mutex;
/* create and initialize the mutex lock */
pthread_mutex_init(&mutex,NULL);
뮤텍스는 pthread_mutex_lock() 및 pthread_mutex_unlock() 함수로 획득 및 해제된다. pthread_mutex_lock()이 호출될 때, 뮤텍스 락을 사용할 수 없는 경우, 소유자가 pthread_mutex_unlock()을 호출할 때까지 호출하는 스레드는 block된다. 다음 코드는 임계 구역을 보호하는 방법을 보여준다:
/* acquire the mutex lock */
pthread_mutex_lock(&mutex);
/* critical section */
/* release the mutex lock */
pthread_mutex_unlock(&mutex);
Pthreads를 구현하는 많은 시스템은 세마포어도 제공하지만, POSIX standard의 일부가 아니며 대신 POSIX SEM 확장에 속한다. POSIX는 named와 unnamed 두 가지 타입의 세마포어를 지정한다. 근본적으로, 두 세마포어는 매우 유사하지만, 프로세스 간에 생성되고 공유되는 방식 측면에서 다르다.
sem_open() 함수는 POSIX named 세마포어를 생성하고 오픈하는 데 사용된다.
#include <semaphore.h>
sem_t *sem;
/* Create the semaphore and initialize it to 1 */
sem = sem_open("SEM", O_CREAT, 0666, 1);
이 경우, 세마포어를 SEM이라고 네이밍한다. O_CREAT 플래그는 세마포어가 아직 존재하지 않으면 세마포어가 생성됨을 나타낸다. 또한, 세마포어는 다은 프로세스에 대한 읽기 및 쓰기 엑세스를 가지고 있으며(파라미터 0666을 통해) 1로 초기화된다.
named 세마포어의 장점은 여러 개의 관련 없는 프로세스가 공통 세마포어를 동기화 메커니즘으로 쉽게 사용할 수 있다는 점이다 세마포어의 이름만 참조하면. 위의 예에서 세마포어 SEM이 생성된 후, 다른 프로세스에 의해 sem_open()을 호출하면 기존 세마포어에 대한 디스크립터가 반환된다.
섹션 6.6에서 설명한 wait()과 signal()이 POSIX에서 각각 sem_wait()과 sem_post()로 선언된다. 다음 코드 샘플은 위에서 만든 named 세마포어를 사용하여 임계 구역을 보호하는 것을 보여준다:
/* acquire the semaphore */
sem_wait(sem);
/* critical section */
/* release the semaphore */
sem_post(sem);
Linux와 macOS 시스템 모두 POSIX named 세마포어를 제공한다.
unnamed 세마포어는 sem_init() 함수를 사용하여 초기화되고, 이 함수에 세 개의 파라미터가 전달된다:
다음 프로그래밍 예제에서 이를 설명한다:
#include <semaphore.h>
sem_t sem;
/* Create the semaphore and initialize it to 1 */
sem_init(&sem, 0, 1);
이 예에서 플로그 0을 전달함으로써, 이 세마포어는 공유될 수 있음을 나타낸다 오직 해당 세마포어를 생성한 프로세스에 속한 스레드에서만.(0이 아닌 값을 제공하면, 세마포어를 별도의 프로세스 간에 공유할 수 있다 공유 메모리 영역에 배치하여.) 또한 세마포어 값을 1로 초기화한다.
POSIX unnamed 세마포어는 named 세마포어와 동일한 sem_wait()과 sem_post()를 사용한다. 다음 코드 샘플은 위에서 만든 unnamed 세마포어를 사용하여 임계 구역을 보호하는 방법을 보여준다:
/* acquire the semaphore */
sem_wait(&sem);
/* critical section */
/* release the semaphore */
sem_post(&sem);
Pthread는 일반적으로 C 프로그램에서 사용되고 C에는 모니터가 없으므로 조건 변수를 뮤텍스 락과 연결하여 lock을 수행한다.
Pthread의 조건 변수는 pthread_cond_t 데이터 타입을 사용하고, pthread_cond_init() 함수를 사용하여 초기화된다. 다음 코드는 조건 변수와 연관된 뮤텍스 락을 생성하고 초기화한다:
pthread_mutex_t mutex;
pthread_cond_t cond var;
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond var,NULL);
pthread_cond_wait() 함수는 조건 변수를 기다리는 데 사용된다. 다음 코드는 스레드가 Pthread 조건 변수를 사용하여 조건 a==b가 true가 될 때까지 기다리는 방법을 보여준다:
pthread_mutex_lock(&mutex);
while (a != b)
pthread_cond_wait(&cond var, &mutex);
pthread_mutex_unlock(&mutex);
조건 변수와 연관된 뮤텍스 락은 pthread_cond_wait() 함수가 호출되기 전에 lock되어야 한다. 이는 조건 절의 데이터를 가능한 race condition으로부터 보호하는 데 사용되기 때문이다. 이 lock이 획득되면, 스레드는 조건을 체크할 수 있다. 조건이 true가 아니면, 스레드는 pthread_cond_wait()을 호출하고, 뮤텍스 락과 조건 변수를 파라미터로 전달한다. pthread_cond_wait()을 호출하면 뮤텍스 락이 해제되어, 다른 스레드가 공유 데이터를 엑세스하고 값을 업데이트하여 조건 절이 true로 평가되도록 할 수 있다.(프로그램을 오류로부터 보호하려면, 조건 절을 루프 내에 위치시켜서 신호가 전달된 후 조건을 다시 체크되도록 하는 것이 중요하다.)
공유 데이터를 수정하는 스레드는 pthread_cond_signal() 함수를 호출하여, 조건 변수에서 대기 중인 스레드 하나를 신호할 수 있다. 이는 아래에 설명되어 있다:
pthread_mutex_lock(&mutex);
a = b;
pthread_cond_signal(&cond var);
pthread_mutex_unlock(&mutex);
pthread_cond_signal() 호출은 뮤텍스 락을 해제하지 않는 다는 점이 중요하다. 뮤텍스 락을 해제하는 것은 그 다음 pthread_mutex_unlock() 호출이다. 뮤텍스 락이 해제되면, 신호를 받은 스레드가 뮤텍스 락의 소유자가 되고 호출에서 pthread_mutex_wait()로 제어를 반환한다.