이번 장에서는 실제 코드들을 예로 사용하여 병행성 문제들을 살펴보고 어떤 문제들을 조심해야 하는지 보도록 하겠다.
핵심 질문: 일반적인 병행성 관련 오류들을 어떻게 처리하는가
병행성 버그는 몇 개의 전형적인 패턴을 갖고 있다.
튼튼하고 올바른 병행 코드를 작성하기 위한 가장 첫 단계는 어떤 경우들을 피해야 할지 파악하는 것이다.
복잡한 병행 프로그램에서 발생하는 병행성 오류들은 어떤 것들이 있는가?
// Thread 1::
if (thd−>proc_info) {
...
fputs(thd−>proc_info, ...) ;
...
}
// Thread 2::
thd−>proc_info = NULL;
thd
자료구조의 proc_info
필드를 두개의 다른 쓰레드가 접근한다.proc_info
가 NULL 인지 검사를 하긴 하지만, fputs
가 실행되기 직전에 인터럽트가 될 수 있다.proc_info
를 NULL로 처리하면 Tread 1로 돌아왔을 때 NPE가 발생할 수 있다.원자성 위반
"다수의 메모리 참조 연산들 간에 있어 예상했던 직렬성(serializability)이 보장되지 않았다."
즉, 코드의 일부에 원자성이 요구되었으나, 실행 시 그 원자성이 위배되었다.
proc_info
필드에 접근할 때 락을 추가하면 된다.
pthread_mutex_t proc_info_lock = PTHREAD_MUTEX_INITIALIZER;
// Thread 1::
pthread_mutex_lock(&proc_info_lock);
if (thd−>proc_info) {
...
fputs(thd−>proc_info, ...) ;
...
}
pthread_mutex_unlock(&proc_info_lock);
// Thread 2::
pthread_mutex_lock(&proc_info_lock);
thd−>proc_info = NULL;
pthread_mutex_unlock(&proc_info_lock);
// Thread 1::
void init() {
...
mThread = PR_CreateThread(mMain , ...) ;
...
}
// Thread 2::
void mMain (...) {
...
mState = mThread−>State;
...
}
위의 예에서 Thread 2는 Thread 1이 실행되었음을 가정한다.
만약 Thread 1이 먼저 실행되지 않았다면 Thread 2에서는 NPE, 또는 임의의 메모리 주소에 접근하게 된다.
해결 방법: 컨디션 변수를 사용하여 "순서를 강제"
pthread_mutex_t mtLock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t mtCond = PTHREAD_COND_INITIALIZER;
int mtInit = 0;
// Thread 1::
void init() {
...
mThread = PR_CreateThread(mMain, ...) ;
// 쓰레드가 생성되었다는 것을 알리는 시그널 전달...
pthread_mutex_lock(&mtLock);
mtInit = 1;
pthread_cond_signal(&mtCond);
pthread_mutex_unlock(&mtLock);
...
}
// Thread 2::
void mMain (...) {
...
// 쓰레드가 초기화되기를 대기...
pthread_mutex_lock(&mtLock);
while (mtInit == 0)
pthread_cond_wait(&mtCond , &mtLock);
pthread_mutex_unlock(&mtLock);
mState = mThread−>State;
...
}
mtInit
이 1이 되므로 정상적으로 실행된다.mtInit
이 0이라 wait()
루틴이 실행되어 초기화되기를 대기한다. 이후 Thread 1에서 초기화가 되면 잠든 쓰레드를 깨우며 잘 작동한다.참고) 비 교착 상태 오류의 대부분(97%)은 원자성 또는 순서 위반에 대한 것이었다.
교착상태(Deadlock): 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태
그렇다면 교착상태를 방지하기 위해서는 어떻게 코드를 작성해야 할까?
교착상태는 왜 발생할까?
이 네 조건이 모두 만족해야 교착상태가 발생한다. 하나라도 만족시키지 않는다면 교착상태는 발생하지 않는다.
먼저 교착 상태를 예방할 수 있는 기술들을 먼저 살펴보자. 각 전략들은 위의 조건들이 발생하는 것을 막는다.
mapping->tree_lock
획득 전에 swap_lock
획득해야 함swap_lock
획득 전에 private_lock
획득해야 함 private_lock
획득 전에 i_mmap_mutex
획득해야 함i_mmap_mutex
획득 전에 i_mutex
획득해야함lock(prevention);
lock(L1);
lock(L2);
...
unlock(prevention);
prevention
락을 획득하고, 그 후에 L1,L2... 락들을 획득여러 락을 획득하는 것에는 문제의 소지가 있다. 왜냐하면 락을 보유한 채로 다른 락을 대기하기 때문이다.
많은 쓰레드 라이브러리들은 이러한 상황을 피할 수 있도록 유연한 인터페이스들을 만들어 놓았다.
trylock()
루틴
top:
lock(L1);
if (trylock(L2) == −1) {
unlock(L1);
goto top;
}
상호 배제 자체를 없애는 기법... 어떻게?
Wait-free 자료구조
// Compare-And-Swap 명령어
int CompareAndSwap(int *address, int expected, int new) {
if (*address == expected) {
*address = new;
return 1; // 성공
}
return 0; // 실패
}
// 값을 amount 만큼 증가시키는 함수
void AtomicIncrement(int *value , int amount) {
do {
int old = *value;
} while (CompareAndSwap(value, old, old + amount) == 0);
}
old
의 값을 바꿨다면, expected
값이 실제 값과 다를 것이므로 실패한다. (교착상태 X)void insert(int value) {
node_t *n = malloc(sizeof(node_t));
assert(n != NULL);
n−>value = value;
// 여기에 락을 걸면 되긴함
n−>next = head;
head = n;
// 여기에 락을 걸면 되긴함
}
next
값은 head
인 A를 갖게 될 것이다.head = n
이 실행되기 전에 인터럽트 발생!head
는 Y가 될 것이다.head = n
이 수행되면 [X, A, B, C]가 되어 Y가 사라진다.void insert(int value) {
node_t *n = malloc(sizeof(node_t));
assert(n != NULL);
n−>value = value;
do {
n−>next = head;
} while (CompareAndSwap(&head , n−>next , n) == 0);
}
어떤 상황에서는 교착 상태를 예방하는 대신 회피하는 것이 더 유용할 때가 있다.
이 방법은 병행성 제약을 가져올 수 있기도 하고, 상당히 제한적인 환경에서만 유용한 방법이다.
교착상태 발생을 허용하고, 교착 상태를 발견하면 복구하도록 하는 방법이다.