스레드 동기화의 이점 - 귀속 자물쇠와 비귀속 자물쇠
가장 흔히 볼 수 있는 프로세스/루틴의 동기화 방법은 상호 배율 자물쇠(또는 상호 배율 Mutex), 읽기와 쓰기 자물쇠(rdlock), 조건 변수(cond), 신호량(Semophore) 등이 있다.Windows 시스템에서도 크리티컬 섹션(Critical Section)과 이벤트 객체(Event)를 동기화하는 데 자주 사용됩니다.
간단하게 말하면 상호 배척 자물쇠는 임계 구역을 보호하는데 이 임계 구역에서 한 번에 최대 한 라인만 들어갈 수 있다.만약 여러 프로세스가 같은 임계 구역에서 활동한다면, 경쟁 조건 (racecondition) 으로 인해 오류가 발생할 수 있습니다.
읽기와 쓰기 자물쇠는 넓은 의미의 논리적으로도 일종의 공유판의 상호 배척 자물쇠라고 볼 수 있다.만약에 임계 구역의 대부분이 읽기 조작이고 소량의 쓰기 조작만 있다면 읽기와 쓰기 자물쇠는 어느 정도에 라인의 상호 배척이 발생하는 대가를 낮출 수 있다.
조건 변수는 어떤 조건이 발생하기를 경쟁이 없는 방식으로 기다릴 수 있도록 허용한다.이 조건이 발생하지 않았을 때, 라인은 줄곧 휴면 상태에 있을 것이다.다른 스레드 알림 조건이 이미 발생했을 때, 스레드가 깨어나 계속 아래로 실행됩니다.조건 변수는 비교적 밑바닥의 동기화 원어로 직접 사용하는 경우가 많지 않고 종종 고위층 간의 루틴 동기화를 실현하는 데 쓰인다.조건 변수를 사용하는 대표적인 예는 스레드 풀(Thread Pool)이다.
운영체제의 프로세스 동기화 원리를 배울 때 가장 많이 말하는 것은 신호량이다.신호량을 정성껏 설계한 PV 조작을 통해 매우 복잡한 프로세스 동기화 상황을 실현할 수 있다(예를 들어 고전적인 철학가의 식사 문제와 이발소 문제).현실적인 프로그래밍에서는 신호를 사용하는 사람이 극히 적다.신호량으로 해결할 수 있는 문제는 항상 다른 더욱 명확하고 간결한 디자인 수단으로 신호량을 대체할 수 있을 것 같다.
이 시리즈의 목적은 이러한 동기화 방법을 어떻게 사용해야 하는지를 설명하기 위한 것이 아니다.더 많은 것은 사람들이 소홀히 하기 쉬운 자물쇠에 대한 개념과 비교적 고전적인 사용과 디자인 방법을 설명하는 것이다.문장은 귀속 자물쇠와 비귀속 자물쇠(recursive mutex와non-recursive mutex), 지역 자물쇠(Scoped Lock), 전략 자물쇠(Strategized Locking), 읽기 자물쇠와 조건 변수, 이중 검출 자물쇠(DCL), 자물쇠와 무관한 데이터 구조(Locking free), 자전거 자물쇠 등 내용을 다루고 벽돌을 던져 옥을 끌어올리기를 희망한다.
그러면 우리는 먼저 귀속 자물쇠와 비귀속 자물쇠에서 떠나자.)
1
1.1
모든 스레드 동기화 방법 중, 아마도 상호 배척 자물쇠 (mutex) 의 등장률은 다른 방법보다 훨씬 높을 것이다.상호 배척 자물쇠의 이해와 기본적인 사용 방법은 모두 매우 쉬우니 여기는 더 이상 소개하지 않겠습니다.
Mutex는 귀속 자물쇠(recursive mutex)와 비귀속 자물쇠(non-recursive mutex)로 나눌 수 있다.귀속 가능한 자물쇠는 다시 들어갈 수 있는 자물쇠(reentrantmutex)라고도 할 수 있으며, 귀속 가능한 자물쇠가 아니면 다시 들어갈 수 없는 자물쇠(non-reentrantmutex)라고도 부른다.
양자의 유일한 차이점은 같은 라인에서 여러 번 같은 귀속 자물쇠를 얻을 수 있고 사쇄가 생기지 않는다는 것이다.한 라인이 같은 비귀속 자물쇠를 여러 번 가져오면 사물쇠가 생성됩니다.
Windows의 Mutex 및 Critical Section은 반복적으로 제공됩니다.Linux의 pthread_mutex_t 자물쇠는 기본적으로 비귀속이다.표시 가능한 설정 PTHREAD_MUTEX_RECURSIVE 속성, pthread_mutex_t는 귀속 자물쇠로 설정됩니다.
대부분의 배척량을 어떻게 사용하는지 소개하는 문장과 책에서 이 두 개념은 종종 무시되거나 얼렁뚱땅 묘사되어 많은 사람들이 이 개념을 전혀 알지 못하게 한다.그러나 이 두 종류의 자물쇠를 잘못 사용하면 프로그램의 자물쇠가 사라질 수 있다.아래 절차를 보십시오.
MutexLock mutex;
void foo() { mutex.lock(); //do something mutex.unlock(); }
void bar() { mutex.lock(); //do something foo(); mutex.unlock(); }
foo 함수와bar 함수는 같은 자물쇠를 가져오고bar 함수는foo 함수를 호출합니다.만약 MutexLock 자물쇠가 비귀속 자물쇠라면, 이 프로그램은 즉시 자물쇠를 닫을 것입니다.따라서 프로그램에 자물쇠를 채울 때 각별히 조심해야 한다. 그렇지 않으면 이런 호출 관계로 인해 자물쇠가 사라지기 쉽다.
요행 심리는 존재하지 마라. 이런 상황은 매우 드물다고 생각한다.코드가 어느 정도 복잡하고 여러 사람에게 유지보수되고 호출 관계가 복잡하게 뒤섞일 때 프로그램에서 이런 오류를 범하기 쉽다.다행히도 이런 원인으로 인한 자물쇠는 쉽게 제거된다.
그러나 이것은 비귀속 자물쇠 대신 귀속 자물쇠를 사용해야 한다는 것을 의미하지는 않는다.귀속 자물쇠는 사용하기에는 비록 간단하지만, 왕왕 일부 코드 문제를 숨길 수 있다.예를 들어 호출 함수와 피호출 함수는 자신이 자물쇠를 얻은 줄 알고 같은 대상을 수정하는데 이때 문제가 생기기 쉽다.따라서 비귀속 자물쇠를 사용할 수 있는 상황에서 가능한 한 비귀속 자물쇠를 사용해야 한다. 왜냐하면 사쇄는 상대적으로 디버깅을 통해 발견하기 쉽기 때문이다.프로그램 설계에 문제가 있으면 노출이 빠를수록 좋다.
1.2
상술한 상황으로 인한 자물쇠를 피하기 위해 AUPE v2는 제12장에서 디자인 방법을 제시했다.즉, 만약에 하나의 함수가 이미 잠긴 상태에서 사용할 수도 있고, 잠기지 않은 상황에서 사용할 수도 있다면, 왕왕 이 함수를 두 가지 버전으로 분해한다. 잠금 버전과 잠금 버전 (nolock 접두사 추가) 이다.
예를 들어foo() 함수를 두 함수로 분해합니다.
//잠금 해제 버전void foo_nolock () {//do something}//자물쇠 버전void fun () {mutex.lock ();foo_nolock ();mutex.unlock ();}
인터페이스의 장래 확장성을 위해 bar () 함수를 같은 방법으로 bar_로 분해할 수 있습니다withou_lock () 함수와 bar () 함수입니다.
Douglas C. Schmidt(ACE 프레임워크의 주요 작성자)의'Strategized Locking,Thread-safe Interface, and Scoped Locking'논문에서 C++를 바탕으로 하는 스레드 보안 인터페이스 모델(Thread-safe interface pattern)을 제시했는데 AUPE의 방법과 이곡동공의 묘가 있다.즉, 인터페이스를 설계할 때 모든 함수도 두 개의 함수로 분해되고 자물쇠를 사용하지 않은 함수는private나protected 형식이며, 자물쇠를 사용하는 함수는public 형식이다.인터페이스는 다음과 같습니다.
class T {public:foo();//잠금 bar ();//잠금 private:foo_nolock();bar_nolock();}
대외 인터페이스인public 함수는 자물쇠가 없는 개인 변수 함수만 호출할 수 있고 서로 호출할 수 없습니다.함수의 구체적인 실현에 있어서 이 두 가지 방법은 기본적으로 같다.
위에서 말한 두 가지 방법은 통상적인 상황에서 문제가 없으며, 효과적으로 자물쇠가 사라지는 것을 피할 수 있다.그러나 일부 복잡한 리셋 상황에서는 반드시 귀속 자물쇠를 사용해야 한다.예를 들어foo 함수는 외부 라이브러리의 함수를 호출하고 외부 라이브러리의 함수는 bar () 함수를 리셋합니다. 이때 반드시 귀속 자물쇠를 사용해야 합니다. 그렇지 않으면 자물쇠가 사라집니다.AUPE라는 책은 12장에서 반드시 귀속 자물쇠를 사용해야 하는 프로그램의 예를 들었다.
1.3
읽기 및 쓰기 잠금 (예: Linux의 pthread_rwlock_t) 은 상호 배타적 잠금보다 높은 수준의 동시 액세스를 제공합니다.읽기와 쓰기 자물쇠의 실현은 왕왕 상호 배척 자물쇠보다 복잡하기 때문에 비용도 통상적으로 상호 배척 자물쇠보다 크다.나의 Linux 기계에서 실험을 통해 단순한 자물쇠를 쓰는 시간 지출 차이가 서로 자물쇠를 열 배 정도 줄이는 것이 아니라는 것을 발견하였다.
시스템이 읽기와 쓰기 자물쇠를 지원하지 않을 때, 때때로 스스로 실현해야 하며, 통상적으로 조건 변수에 읽기와 쓰기 계수기로 실현된다.때로는 실제 상황에 따라 독자 우선이나 작성자 우선의 읽기와 쓰기 자물쇠를 실현할 수 있다.
읽기와 쓰기 자물쇠의 장점은 읽기 조작이 빈번하고 쓰기 조작이 적은 상황에서 나타난다.만약에 쓰기 작업의 횟수가 읽기 작업보다 많고 쓰기 작업의 시간이 매우 짧다면 프로그램의 대부분 비용은 읽기와 쓰기 자물쇠에 쓰인다. 이때 오히려 상호 배척 자물쇠를 사용하면 효율이 더욱 높아진다.
많은 학생들이 읽기와 쓰기 자물쇠의 기본적인 사용 방법을 배운 후에 아래와 같은 프로그램을 쓴 적이 있다고 믿는다.
#include
/* 프로그램 2*/#include
프로그램 1이 먼저 자물쇠를 읽고 나중에 자물쇠를 넣는 것은 이치상 막아야 하지만 프로그램은 순조롭게 실행될 수 있다는 것을 의심스럽게 발견할 수 있다.프로그램 2는 막혔다.
한 걸음 더 가까이, 당신은 아래의 프로그램 3과 프로그램 4를 실행하면 무슨 일이 일어날지 말할 수 있습니까?
/* 프로그램 3*/#include
POSIX 표준에서 한 라인이 먼저 쓰기 자물쇠를 얻고 읽기 자물쇠를 얻으면 결과는 예측할 수 없다.이것이 바로 프로그램 1의 운행이 예상했던 이유이다.주의해야 할 것은 읽기 자물쇠는 귀속 자물쇠(다시 들어갈 수 있음), 쓰기 자물쇠는 귀속 자물쇠(즉 다시 들어갈 수 없음)이다.따라서 프로그램 3은 잠기지 않고 프로그램 4는 계속 막힌다.
읽기와 쓰기 자물쇠의 귀속 여부는 플랫폼에 따라 다를 수 있으므로 혼동을 피하기 위해 잘 모르는 상황에서 같은 라인에서 읽기와 쓰기 자물쇠를 혼용하는 것을 피하는 것을 권장합니다.
시스템이 귀속 자물쇠를 지원하지 않고 사용해야 할 때, 스스로 귀속 자물쇠를 만들어야 한다.일반적으로 귀속 자물쇠는 비귀속 상호 배척 자물쇠에 인용 계수기를 추가하여 실현된다.간단하게 말하면 자물쇠를 채우기 전에 먼저 자물쇠를 넣은 라인과 현재 자물쇠를 넣은 라인이 동일한지 판단한다.같은 라인이라면, 계수기를 인용하여 1을 추가합니다.만약 그렇지 않다면, 인용 계수기를 1로 설정하고, 현재 스레드 번호를 기록하고, 자물쇠를 채운다.하나의 예는 이곳을 볼 수 있다.주의해야 할 것은 귀속 자물쇠를 공용 라이브러리로 사용하려면 더 많은 이상 상황과 오류 처리를 고려하여 코드를 더욱 튼튼하게 해야 한다는 것이다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
제한된 크기의 디렉토리를 만드는 방법오늘 저는 장치에 공간이 없을 때 백업 중에 응용 프로그램이 어떻게 작동하는지 테스트(및 수정)하는 작업이 있습니다. 결과적으로 "남은 공간 없음"오류로 백업이 실패하면 새 파일이 없어야 합니다. 지금까지 문제를 재...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.