[OS] 26. Common Concurrency Problems

Park Yeongseo·2024년 2월 2일
post-thumbnail

Operating Systems : Three Easy Pieces를 보고 번역 및 정리한 내용들입니다.

이 장에서는 실제 코드에서 볼 수 있는 몇 가지 병행성 문제들의 예시들에 대해 간단히 살펴보도록한다.

1. What Types Of Bugs Exists?

가장 첫 번째 물음은 다음과 같다. 복잡한 병행 프로그램들에서 나타나는 병행성 버그에는 어떤 종류가 있을까? 일반적으로 이 물음에 대해 답하는 것은 어렵지만, 다행히도 이 물음에 대해 이미 답을 해준 사람들이 있다. 구체적으로는 많은 병행 응용 프로그램들에 대해 자세히 분석했던 Lu와 그 동료들의 연구를 통해 어떤 종료의 버그들이 실제로 일어나는지에 대해 이해해본다.

이 연구는 네 개의 중요한 오픈 소스 어플리케이션들(MySQL, Apache, Mozilla, OpenOffice)에 초점을 맞추고 있다. 저자들은 연구에서 각 코드 베이스에서 발견되어 고쳐진 병행성 버그들에 대한 정량적인 분석을 수행했는데, 그 결과를 이해한다면 실제로 성숙한 코드에서 어떤 종류의 문제가 발생하는지에 대해 잘 이해할 수 있을 것이다.

다음의 표는 Lu와 그 동료들이 연구한 버그들에 대한 요약이다. 여기에서는 총 105개의 버그가 있는데, 그 중 대부분(74)은 데드락이 아님을 볼 수 있다. 또한 여기에서는 각 어플리케이션에서 연구된 버그의 수도 볼 수 있는데, OpenOffice는 고작 총 8개의 병행성 버그밖에 없지만, Mozilla에서는 거의 60개에 가깝다는 것도 볼 수 있다.

이제는 이 여러 버그 클래스들에 대해서 더 깊이 알아보도록 하자. 첫 번째 클래스인 비-데드락 버그들에 대해서는 연구의 예제들을 사용할 것이다. 두 번째, 데드락 버그들의 경우에는 데드락 예방, 회피, 처리를 위해 진행되어 온 오랜 작업들에 대해 논의하게 될 것이다.

2. Non-Deadlock Bugs

Lu의 연구에 따르면 병행성 버그의 대부분은 비-데드락 버그들이다. 그런데 이러한 타입의 버그들이 무엇을 말하는 것일까? 이 버그들은 어떻게 일어날까? 이것들은 어떻게 고칠 수 있을까? Lu가 찾은 비-데드락 버그들의 두 대표 타입들에 대해 논의해보자. 그 중 하나는 원자성 위반(atomicity violation)이고, 나머지 하나는 순서 위반(order viloation)이다.

Atomicity-Violation Bugs

첫 번째 문제는 원자성 위반이라 불린다. MySQL에서의 간단한 예시를 보자. 설명을 읽기 전에 어떤 버그가 일어나고 있는지에 대해 고민해보도록 하자.

Thread1::
if (thd->proc_info){
	fputs(thd->proc_info, ...);
}

Thread2::
thd->proc_info = NULL;

이 예시에서 두 스레드는 구조체 thd의 필드 proc_info에 접근하려 한다. 첫 번째 스레드는 값이 NULL이 아닌지를 확인한 후 그 값을 출력하고, 두 번쨰 스레드는 값을 NULL로 설정한다. 만약 첫 번째 스레드가 값 확인을 수행하고 나서 fputs를 호출하기 전에 인터럽트가 발생하고, 두 번쨰 스레드가 그 값을 NULL로 설정하게 된다면, 첫 번째 스레드가 재실행되었을 때 크래시가 발생하게 될 것이다.

Lu에 따르면, 원자성 위반에 대한 좀 더 형식적인 정의는 다음과 같다. "여러 메모리 접근 사이에서 기대되는 직렬성이 위반되는 것." 위의 예시에서 코드는 NULL이 아닌 proc_info에 대한 확인과 fputs에서의 proc_info 사용에 대해 원자성을 가정하고 있다. 하지만 이 가정은 틀렸고, 코드는 잘 작동하지 않는다.

아래의 해결 코드에서는 공유 변수 참조 주변에 락을 추가함으로써, 스레드가 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);

Order-Violation Bugs

Lu가 발견한, 나머지 흔한 비-데드락 버그 타입은 순서 위반으로 알려져 있다. 여기에 또 간단한 예제가 있다. 어떤 버그가 있을지 다시 직접 생각해보자.

Thread 1::
void init() {
	mThread = PR_CreateThread(mMain, ...);
}

Thread 2::
void mMain(...) {
	mState = mThread->State;
}

코드를 보면 알 수 있듯, Thread 2의 코드는 변수 mThread가 이미 초기화되어 NULL이 아님을 가정하고 있다. 하지만 만약 Thread 2가 생성되자마자 실행되는 경우를 생각하면, mThread의 값은 초기화되어 있지 않을 것이고, 또 NULL 포인터 역참조로 인해 크래시를 일으킬 것이다. 단 여기서 mThread의 값이 처음에는 NULL이라 가정했다는 것에 주의하자. 만약 이렇게 하지 않았다면 Thread 2의 역참조에 의해 임의의 메모리 위치에 접근하게 될 것이므로, 더 이상한 일들이 일어날 수도 있다.

순서 위반에 대한 좀 더 형식적인 정의는 다음과 같다. "두 메모리 접근에 있어 기대되는 순서가 서로 뒤바뀌는 것"

일반적으로 이러한 종류의 버그의 수정은 순서를 강제함으로써 이루어진다. 이전에 논의한 것처럼, 조건 변수를 사용하는 것은 오늘날의 코드에 이와 같은 스타일의 동기화를 추가하는, 쉬우면서도 견고한 방법이다. 따라서 위의 코드는 아래와 같이 다시 쓸 수 있다.

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, ...);
	
	// signal that the thread has been created...
	pthread_mutex_lock(&mtLock);
	mtInit = 1;
	pthread_cond_signal(&mtCond);
	pthread_mutex_unlock(&mtLock);
	...
}

Thread 2::
void mMain(...) {
	...
	// wait for the thread to be initialized...
	pthread_mutex_lock(&mtLock);
	while (mtInit == 0)
	pthread_cond_wait(&mtCond, &mtLock);
	pthread_mutex_unlock(&mtLock);
	
	mState = mThread->State;
	...
}

이 수정된 코드 시퀀스에서는, 조건 변수(mtCond)와 그에 맞는 락(mtLock), 그리고 상태 변수(mtInit)을 추가했다. 초기화 코드가 실행되면 이는 mtInit의 상태를 1로 바꾸고 시그널을 보낸다. 만약 Thread 2가 이 시점 이전에 실행된다면, 이 스레드는 해당 시그널과 그에 대응하는 상태가 변하기를 기다릴 것이다. 만약 나중에 다시 실행되면, 이 스레드는 상태를 확인하고 초기화가 이미 일어났는지, 즉 mtInit이 1로 설정되었는지를 확인하고, 만약 그렇다면 계속 실행될 것이다. mThread를 상태 변수로 사용할 수 있지만, 단순함을 위해 그렇게 하지는 않았음에 주의하자. 스레드 간 순서가 문제가 될 때에는 조건 변수나 세마포어를 사용하는 것이 해결책이 될 수 있다.

Non-Deadlock Bugs: Summary

Lu가 연구한 비-데드락 버그들의 대부분(97%)은 원자성 위반이거나 순서 위반이었다. 그러므로 이러한 버그 패턴들에 대해 주의깊게 생각한다면, 이러한 버그들을 더 잘 피할 수 있을 것이다.

하지만 모든 버그들이 위 예시와 같이 쉽게 고쳐지는 것은 아니다. 몇몇 버그들은 수정을 위해 프로그램이 무얼하는지에 대한 더 깊은 이해와 많은 양의 코드 및 자료 구조 재편성을 필요로 한다.

3. Deadlock Bugs

위에서 언급한 병행성 버그 외에도, 복잡한 락 프로토콜을 사용하는 여러 병행 시스템들에서 나타나는 문제에는 데드락이 있다. 데드락은, 예를 들면, 한 스레드가 락(L1)을 가진 상태에서 다른 락(L2)를 기다리고 있는데, 다른 스레드가 L2를 가지고 L1을 기다리고 있는 상황에서 발생한다. 아래의 코드 스니펫은 그런 잠재적 데드락을 보여준다. 단 이 코드가 실행된다고 해서 반드시 데드락이 일어나는 것은 아님에 주의하자.

Thread 1: 
pthread_mutex_lock(L1); 
pthread_mutex_lock(L2);

Thread 2:
pthread_mutex_lock(L2);
pthread_mutex_lock(L1);

Why Do Deadlocks Occur?

위와 같은 간단한 데드락은 Thread 1과 Thread 2가 락을 같은 순서로 가질 수 있게 함으로써 해결할 수 있을 것처럼 보인다. 그렇다면 데드락은 왜 일어나는 걸까?

한 이유는, 긴 코드에서는 구성 요소들 사이에서 복잡한 의존성이 발생하기 떄문이다. 예를 들어 OS를 생각해보자. 가상 메모리 시스템은 디스크로부터 블럭을 페이지 인 해오기 위해 파일 시스템에 접근해야 한다. 한편 파일 시스템은 블럭을 읽어 들이기 위해 메모리 페이지를 필요로 하고, 따라서 가상 메모리 시스템에 접근해야 한다. 따라서 큰 시스템에서는 코드에서 자연스럽게 일어날 수 있는 환형 의존의 데드락을 피하기 위해 락 전략을 세심하게 설계해야 한다.

다른 이유는 캡슐화(encapsulation) 때문이다. 소프트웨어 개발자로서,우리는 구현의 세부 내용을 숨기고 소프트웨어를 모듈화해 구성하도록 배웠다. 하지만 불행히도 그런 모듈성은 락과는 잘 어울리지 않는다. 보기에는 문제가 없어 보이는 인터페이스가 데드락을 일으킬 수 있다. 예를 들어 자바 Vector 클래스의 메서드 AddAll()을 생각해보자.

Vector v1, v2;
v1.AddAll(v2);

내부적으로, 메서드는 멀티-스레드 안전해야하기 때문에, 두 벡터에 대한 락을 모두 획득해야 한다. 이 루틴은 그런 락들을 임의의 순서로(v1 이후 v2라 하자) 획득한다. 만약 다른 스레드가 v2.AddAll(v1)을 거의 동시에 호출한다면, 데드락이 일어날 잠재적 가능성이 있다.

Conditions for Deadlock

데드락이 일어나려면 네 개의 조건이 만족되어야 한다.

  • 상호 배제(Mutual exclusion)
    + 스레드들은 필요한 자원을 독점적으로 사용한다.
  • 점유 대기(Hold-and-wait)
    + 스레드들은 자신에게 할당된 자원들을 가지고, 추가적인 자원들을 기다린다.
  • 비선점(No Preemption)
    + 자원을 가지고 있는 스레드로부터 해당 자원을 강제로 빼앗을 수 없다.
  • 순환 대기(Circular wait)
    + 스레드가 요구하는 자원들 사이에 순환이 발생해야 한다.

이 네 조건 중 하나라도 충족되지 않으면 데드락은 발생하지 않는다. 따라서 우선은 데드락 발생을 방지하는 테크닉들에 대해서 알아보자. 이 테크닉들은 각각 위 조건 중 하나가 만족되는 일을 방지함으로써 데드락이 발생하지 않도록 한다.

Prevention

Circular Wait

가장 실용적인 방지 테크닉은 아마 순환 대기가 발생하지 않도록 락 코드를 작성하는 방법일 것이다. 이를 위한 가장 직관적인 방법은 락 획득에 대한 전체 순서(total ordering)를 제공하는 것이다. 예를 들어 만약 시스템 내에 오직 두 개의 락이 있다고 한다면, 항상 L1을 L2 전에 획득함으로써 데드락을 방지할 수 있다. 이렇게 순서를 정하면 순환 대기가 발생하지 않게 해 데드락이 발생하지 않도록 할 수 있다.

물론 더 복잡한 시스템에서는 둘보다 더 많은 락이 있을 수 있기에, 전체 락에 대해 순서를 매기는 일은 어려워진다. 따라서 부분 순서(partial ordering)을 제공하는 것이 데드락을 피할 수 있게 락 획득을 구성하는 좋은 방법일 수 있다. 리눅스에서의 메모리 매핑 코드가 부분 순서의 좋은 예시다. 소스 코드의 위쪽에 있는 주석은 열 가지 다른 락 획득 순서 그룹을 보이고 있다.

전체 순서와 부분 순서 모두 락 전략에 대한 세심한 설계를 필요로 한다. 나아가, 순서는 그저 관례일 뿐이기에 숙련되지 않은 프로그래머들은 락 프로토콜을 무시하고 잠재적으로 데드락을 일으킬 수도 있다. 마지막으로, 락 순서는 코드와, 어떻게 다양한 루틴들이 호출되는지에 대한 깊은 이해를 필요로 한다.

Hold-and-wait

점유 대기는 모든 락들을 한 번에, 원자적으로 획득함으로써 피할 수 있다. 다음과 같은 코드를 통해 달성할 수 있다.

pthread_mutex_lock(prevention); // begin acquisition
pthread_mutex_lock(L1);
pthread_mutex_lock(L2);
...
pthread_mutex_unlock(prevention); // end

우선 락 prevention을 얻음으로써 이 코드는 코드 획득 중에 원치 않은 스레드 전환이 이루어지지 않고, 따라서 데드락이 발생하지 않음을 보장한다. 물론 이를 위해서는 어떤 스레드가 락을 잡든, 우선은 전역 prevention 락을 잡아야 한다.

하지만 이 해법이 여러 이유로 문제적이라는 것에 주의하자. 이전에 그러했듯 캡슐화가 문제가 된다. 루틴을 호출할 때, 이 접근법은 어떤 락을 잡아야 하는지 정확히 알아야하고, 그것들을 미리 획득해야한다. 이 테크닉은 또한 모든 락들을, 그것들이 실제로 필요하기 전이 아니라, 미리 한 번에 획득하기를 필요로 하므로 병행성을 감소시킬 수도 있다.

No Preemption

보통 락은 언락이 호출될 때까지 소유되고 있는 것으로 보이기에 다수의 락 획득은 종종 문제가 된다. 락을 가지고 있는 상태에서 다른 것을 얻으려 하는 경우 때문이다. 많은 스레드 라이브러리들은 이러한 상황을 피하기 위해 좀 더 융통성 있는 인터페이스 집합을 제공한다. 구체적으로, 루틴 ptherad_mutex_trylock()은 락을 잡고 성공 코드를 반환하거나, 락이 이미 사용되고 있음을 알리는 에러 코드를 반환한다. 후자의 경우, 해당 락을 잡고 싶다면 나중에 다시 시도할 수 있다.

이러한 인터페이스는 데드락이 없는, 순서와 관련 없는 락 획득 프로토콜을 만들기 위해 다음과 같이 사용될 수 있다.

top:
	pthread_mutex_lock(L1);
	if (pthread_mutex_trylock(L2) != 0) {
		pthread_mutex_unlock(L1);
		goto top;
	}

다른 스레드가 같은 프로토콜을 따르면서도 다른 순서로 락을 잡을 수도 있고, 그럼에도 프로그램에는 데드락이 발생하지 않을 수 있음에 주의하자. 하지만 라이브락(livelock)이라는 새로운 문제가 발생할 수 있다. 보통 그럴 일은 없지만, 두 스레드가 모두 위 시퀀스를 반복하면서도 락 획득에 계속해서 실패하는 경우가 있을 수 있다. 이 경우 두 스레드는 이 루틴을 계속 반복하기 때문에 데드락은 아니지만, 진전은 이루어지지 않는다. 이러한 라이브락 문제에 대해서도 해법들이 있다. 예를 들어 반복문의 처음으로 돌아갈 때 임의의 지연시간을 두면 경쟁 스레드 사이의 반복되는 간섭의 확률을 줄일 수 있다.

이 해법에서 하나 짚고 가야하는 점은, 이것이 trylock 접근법을 사용하는 것의 어려운 부분들은 다루고 있지 않다는 점이다. 다시 발생할 수 있는 첫 번째 문제는 캡슐화 때문에 발생한다. 만약 이 락들 중 하나가 호출되는 루틴 깊숙히 묻혀 있다면, 처음으로 돌아가게 하는 일은 구현하기 더 어려워진다. 만약 코드가 L1이 아닌 다른 자원을 얻게 되면, 처음으로 돌아갈 때에는 해당 자원 또한 반납해야 한다. 예를 들어 L1을 얻은 후 코드가 메모리도 할당했다면, 이 메모리는 L2 획득에 실패해서 처음으로 돌아가 전체 시퀀스를 재실행하기 전에 반납되어야 한다. 다만 이러한 방식은 제한적인 환경에서는 제대로 동작할 수 있다.

Mutual Exclusion

마지막 방지 테크닉은 상호 배제를 완전히 없애는 것이다. 보통 우리가 실행하고자 하는 코드에는 임계 영역이 있어서 이렇게 하는 게 쉽지는 않다. 그렇다면 어떻게 할 수 있을까?

락을 전혀 필요로 하지 않는 여러 자료 구조를 설계할 수 있는데, 이러한 접근법에서 쓰이는 아이디어는 간단하다. 강력한 하드웨어 명령어를 사용함으로써 명시적인 락이 없이 자료 구조를 만드는 것이다.

간단한 예시로, 다음과 같은 원자적 하드웨어 명령을 제공하는 compare-and-swap 명령어를 쓸 수 있다고 가정하자.

int CompareAndSwap(int *address, int expected, int new) {
	if (*address == expected) {
		*address = new;
		return 1; // success
	}
	return 0;
}

이제 원자적으로 compare-and-swap을 이용해 값을 특정 양만큼 증가시키고 싶다고 하자. 다음의 간단한 함수를 이용할 수 있다.

void AtomicIncrement(int *value, int amount){
	do {
		int old = *value;
		while (CompareAndSwap(value, old, old + amount) == 0);
	}
}

이를 이용하면 락을 얻고 업데이트하고 풀어주는 방식을 사용하지 않고도, compare-and-swap을 이용해 값을 갱신할 수 있다. 이러한 방식에서는 락을 사용하지 않으므로 데드락도 발생하지 않는다(라이브락은 발생할 가능성이 있음에 주의).

리스트 삽입과 같은 조금 더 복잡한 예제를 생각해보자. 다음은 리스트의 헤드에 삽입을 하는 코드다.

void insert(int value) {
	node_t *n = malloc(sizeof(node_t));
	assert(n != NULL);
	n->value = value;
	n->next = head;
	head = n;
}

이 코드는 간단한 삽입을 수행하는 코드지만, 다수의 스레드에 의해 동시에 호출되는 경우 경쟁 조건을 가진다. 이러한 문제는 물론 코드를 락 획득 및 해제로 둘러쌈으로써 해결할 수 있다. 하지만 이번에는 그렇게 하지 말고 compare-and-swap을 이용해 해결해보도록 하자.

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

이 코드는 next 포인터가 현재의 헤드를 가리키게 업데이트하고 새로 만들어진 노드를 리스트의 새 헤드가 되도록 만든다. 하지만 이 코드는 만약 어떤 다른 스레드가 도중에 성공적으로 새 헤드를 추가하는 경우, 이 스레드가 새 헤드로 삽입을 재시도하게 하면서 실패한다.

물론 유용한 리스트를 만들기 위해서는 리스트 삽입 이외의 것들도 필요하고, 삽입, 삭제, 검색이 가능한 리스트를 락이 없는 방식으로 만드는 일이 쉬운 일은 아니다.

Deadlock Avoidance via Scheduling

어떤 시나리오에서는 데드락 방지가 아니라, 데드락 회피(avoidance)가 선호되기도 한다. 데드락 회피를 위해서는 다양한 스레드들이 실행 중에 어떤 락을 얻게 될지를 알고, 해당 스레드들을 데드락이 일어나지 않음을 보장하면서 스케줄링 할 수 있어야 한다.

예를 들어, 두 프로세서와 네 스레드들이 있다고 하자. 또한 스레드 T1은 락 L1과 L2, T2도 L1, L2, T3는 L2을 가져야 하고, T4는 어떤 락도 얻지 않는다고 가정하자. 표로 나타내면 다음과 같다.

똑똑한 스케줄러는 T1, T2가 동시에 돌아가지 않는다면 어떠한 데드락도 일어나지 않게 할 수 있다. 다음이 그러한 스케줄링의 한 예다.

T3, T1이 동시에 돌아가거나 T3, T2가 동시에 돌아가는 것은 괜찮다. T3가 락 L2를 얻는다 하더라도, 오직 하나의 락만을 획득하기 때문에 병행적으로 실행되는 다른 스레드들과 데드락을 만들지는 않기 때문이다.

다른 예 하나를 더 보자. 여기에서는 동일한 리소스(L1, L2)에 대한 더 많은 경쟁이 일어난다.

여기에서 스레드 T1, T2, T3는 모두 실행되기 위해 L1, L2를 획득해야한다. 다음은 데드락이 절대 일어나지 않게 하는 한 가지 가능한 스케줄이다.

여기서 볼 수 있듯, 정적 스케줄링은 T1, T2, T3가 모두 한 프로세서에서 돌아가게 하고, 따라서 작업이 완료되게 하기 위한 전체 시간을 상당한 수준으로 늘려버리는 보수적인 접근법으로 이어진다. 이러한 작업들을 병행적으로 실행할 수 있었을 것임에도, 데드락에 대한 걱정으로 이를 포기한 것이다. 대가는 성능이다.

이러한 접근법의 한 유명한 사례로는 Dijkstra의 은행원 알고리즘이 있고, 다른 여러 유사한 접근법들도 있다. 하지만 그것들은, 예를 들면 한 사람이 실행되어야 할 모든 작업 집합과 그 작업들에 필요한 락에 대해 모두 알고 있는 임베디드 시스템과 같은 몹시 제한적인 환경에서나 유용하다. 게다가 그런 접근법들은 위 두 번째 예시에서 본 것과 같이, 병행성을 제한할 수 있다. 따라서 스케줄링을 통한 데드락 회피는 널리 쓰이는 범용적인 해결책은 아니다.

Detect and Recover

마지막 일반적인 전략은 데드락이 때때로 일어나는 것을 허용하고, 그런 데드락이 감지되었을 때 행동을 취하는 것이다. 예를 들어, OS가 1년에 한 번만 멈춰버린다면, 그냥 리부트를 하고 작업을 다시 하면 될 것이다. 만약 데드락이 정말 잘 일어나지 않는다면, 이렇게 해결하지 않는 것도 사실 꽤 실용적이라 할 수 있다.

많은 데이터베이스 시스템은 이런 데드락 감지와 복구 테크닉을 사용한다. 데드락 디텍터는 주기적으로 실행되면서, 자원 그래프를 만들고 사이클을 확인한다. 만약 사이클이 발견된다면, 즉 데드락이 발생한다면 시스템은 재시작된다. 만약 자료구조에 대한 좀 더 복잡한 복구가 필요한 경우에는 사람이 해당 프로세스를 직접 할 수도 있다.

4. Summary

이 장에서는 병행 프로그램에서 발생할 수 있는 버그들의 종류들에 대해 공부했다. 첫 번째, 비-데드락 버그들은 놀랍도록 자주 발생하지만, 보통은 고치기 쉽다. 여기에는 원자성 위반과 순서 위반이 있다.

다음으로 데드락에 대해, 왜 이것이 발생하고, 이를 해결하기 위해서는 어떻게 해야하는지에 대해 간단하게 논의했다. 데드락은 병행성만큼이나 오래된 문제다. 이 문제에 대한 실질적으로 가장 좋은 해결책은 주의를 기울여, 락 획득 순서를 정함으로써 데드락이 발생하는 것을 우선적으로 방지하는 것이다.

락을 쓰지 않는 접근법은, 이제 락이 없는 자료 구조들이 리눅스를 포함해, 널리 쓰이는 라이브러리들과 핵심 시스템들에서 발견되는 만큼, 유망해 보인다. 하지만 보편성이 떨어지고, 락이 없는 새 자료 구조를 만드는 것의 복잡함 때문에, 이 접근법은 전반적으로 제한적인 유용성을 가지고 있다. 아마 가장 좋은 해결책은 새로운 병행 프로그래밍 모델을 개발하는 것이 될 것이다.

0개의 댓글