이중 체크 잠금
Double-checked locking소프트웨어 엔지니어링에서 이중 체크 잠금("이중 체크 잠금 최적화"[1]라고도 함)은 잠금을 획득하기 전에 잠금 기준("잠금 힌트")을 테스트하여 잠금을 획득하는 오버헤드를 줄이는 데 사용되는 소프트웨어 설계 패턴이다.잠금 기준 점검 결과 잠금이 필요한 것으로 나타난 경우에만 잠금이 발생한다.
이 패턴은 일부 언어/하드웨어 조합으로 구현될 경우 안전하지 않을 수 있다.때로는 안티패턴으로 볼 수 있다.[2]
특히 Singleton 패턴의 일부로서 멀티스레드 환경에서 "지속적인 초기화"를 실행할 때 일반적으로 잠금 오버헤드를 줄이는 데 사용된다.느리게 초기화할 경우 처음 액세스할 때까지 값 초기화를 피할 수 있다.
C++11의 사용량
싱글톤 패턴의 경우 다음과 같이 이중 체크된 잠금이 필요하지 않다.
변수가 초기화되는 동안 통제가 동시에 선언문에 들어갈 경우 동시 실행은 초기화가 완료될 때까지 기다려야 한다.
— § 6.7 [stmt.dcl] p4
싱글턴& 겟인스턴스() { 정태의 싱글턴 s; 돌아오다 s; }
C++11 이상에서는 또한 다음과 같은 형태로 내장된 이중 체크 잠금 패턴을 제공한다.std::once_flag
그리고std::call_once
:
#include <<mutex> #include <선택적>// C++17 이후 // 싱글톤.h 계급 싱글턴 { 공중의: 정태의 싱글턴* 겟인스턴스(); 사유의: 싱글턴() = 체납; 정태의 찌꺼기::선택적<싱글턴> s_s.; 정태의 찌꺼기::onest_filency s_s.; }; // Singleton.cpp 찌꺼기::선택적<싱글턴> 싱글턴::s_s.; 찌꺼기::onest_filency 싱글턴::s_s.{}; 싱글턴* 싱글톤::GetInstance() { 찌꺼기::call_one(싱글턴::s_s., []() { s_s..본을 뜨다(싱글턴{}); }); 돌아오다 &*s_s.; }
위의 사소한 작업 예시 대신 이중 체크된 관용어를 진정으로 사용하고자 하는 경우(예를 들어 2015년 출시 전 Visual Studio가 위에서 인용한 동시 초기화에 대한 C++11 표준의 언어를 구현하지 않았기 때문)에는 다음과 같은 펜스를 획득하고 해제해야 한다.[4]
#include <<atomic>> #include <<mutex> 계급 싱글턴 { 공중의: 정태의 싱글턴* 겟인스턴스(); 사유의: 싱글턴() = 체납; 정태의 찌꺼기::원자성의<싱글턴*> s_s.; 정태의 찌꺼기::뮤텍스 s_xx; }; 싱글턴* 싱글톤::GetInstance() { 싱글턴* p = s_s..짐을 싣다(찌꺼기::memory_order_message); 만일 (p == 무효의) { // 1차 점검 찌꺼기::lock_guard<찌꺼기::뮤텍스> 자물쇠를 채우다(s_xx); p = s_s..짐을 싣다(찌꺼기::memory_order_message); 만일 (p == 무효의) { // 2차(이중) 점검 p = 새로운 싱글턴(); s_s..저장하다(p, 찌꺼기::memory_order_release); } } 돌아오다 p; }
이동 시 사용량
꾸러미 본래의 수입하다 "sync" 시합을 하다 ArrOnce 동기를 맞추다.한번 시합을 하다 arr []인트로 // getArr는 첫 번째 통화 시 느리게 초기화를 검색한다.이중 체크 // 동기화와 함께 잠금이 구현된다.한 번 라이브러리 기능.맨 처음 것, 제1; 전자 // Do()를 호출하기 위한 경쟁에서 이기기 위한 고루틴은 배열을 초기화하는 동안 // 다른 이들은 도()가 완료될 때까지 차단한다.Do가 실행된 후에는 // 배열을 얻으려면 단일 원자 비교가 필요하다. 펑크 게타르() []인트로 { ArrOnce.하다(펑크() { arr = []인트로{0, 1, 2} }) 돌아오다 arr } 펑크 본래의() { // 이중 체크 잠금 기능 덕분에 getArr()를 시도하는 두 개의 고루틴 // 이중 패치를 유발하지 않음 가다 게타르() 가다 게타르() }
자바에서의 사용법
예를 들어, Java 프로그래밍 언어에서 (다른 모든 Java 코드 세그먼트뿐만 아니라)가 제공하는 코드 세그먼트를 고려하십시오.
// 단일 스레드 버전 계급 푸 { 사유의 정태의 도우미 조력자; 공중의 도우미 겟헬퍼() { 만일 (조력자 == 무효의) { 조력자 = 새로운 도우미(); } 돌아오다 조력자; } // 기타 기능 및 구성원... }
문제는 여러 개의 스레드를 사용할 때 이것이 작동하지 않는다는 것이다.두 개의 스레드가 호출될 경우 잠금 장치를 확보해야 함getHelper()
동시에그렇지 않으면 둘 다 동시에 객체를 만들려고 하거나 불완전하게 초기화된 객체에 대한 참조를 얻을 수 있다.
자물쇠는 다음 예시와 같이 값비싼 동기화를 통해 얻는다.
// 수정되었지만 비용이 많이 드는 멀티스레드 버전 계급 푸 { 사유의 도우미 조력자; 공중의 동기화된 도우미 겟헬퍼() { 만일 (조력자 == 무효의) { 조력자 = 새로운 도우미(); } 돌아오다 조력자; } // 기타 기능 및 구성원... }
하지만, 첫 번째 방문은getHelper()
객체를 만들 것이며, 그 시간 동안 객체에 접근하려고 시도하는 몇 개의 스레드만 동기화하면 된다. 그 이후에는 모든 통화는 멤버 변수에 대한 참조만 얻는다.극단적인 경우 방법을 동기화하면 성능이 100배 이상 저하될 수 있으므로,[5] 이 방법을 호출할 때마다 잠금 장치를 획득하고 해제하는 오버헤드는 불필요해 보인다. 초기화가 완료되면 잠금 장치를 획득하고 해제하는 것은 불필요해 보인다.많은 프로그래머들은 다음과 같은 방법으로 이 상황을 최적화하려고 시도했다.
- (잠금을 얻지 않고) 변수가 초기화되었는지 확인하십시오.초기화된 경우 즉시 반환하십시오.
- 자물쇠를 따다.
- 변수가 이미 초기화되었는지 다시 확인하십시오. 다른 스레드가 먼저 잠금을 획득한 경우 초기화를 이미 수행했을 수 있음.그렇다면 초기화된 변수를 반환하십시오.
- 그렇지 않으면 변수를 초기화하고 반환하십시오.
// 다중 스레드 버전 깨짐 // "이중 체크 잠금" 숙어 계급 푸 { 사유의 도우미 조력자; 공중의 도우미 겟헬퍼() { 만일 (조력자 == 무효의) { 동기화된 (이) { 만일 (조력자 == 무효의) { 조력자 = 새로운 도우미(); } } } 돌아오다 조력자; } // 기타 기능 및 구성원... }
직관적으로 이 알고리즘은 문제에 대한 효율적인 해결책처럼 보인다.그러나 이 기술은 많은 미묘한 문제를 가지고 있으므로 대개 피해야 한다.예를 들어 다음과 같은 이벤트 순서를 고려하십시오.
- 스레드 A는 값이 초기화되지 않았음을 인지하여 잠금을 얻고 값을 초기화하기 시작한다.
- 일부 프로그래밍 언어의 의미론 때문에 컴파일러에서 생성된 코드는 A가 초기화 수행을 마치기 전에 부분적으로 구성된 객체를 가리키도록 공유 변수를 업데이트할 수 있다.예를 들어, Java에서 생성자에 대한 호출을 인라인 처리한 경우, 스토리지가 할당되고 인라인 처리된 생성자가 객체를 초기화하기 전에 공유 변수가 즉시 업데이트될 수 있다.[6]
- 스레드 B는 공유 변수가 초기화(또는 표시되도록)되었음을 감지하고 값을 반환한다.스레드 B는 값이 이미 초기화되었다고 믿기 때문에 잠금을 획득하지 않는다.A에 의해 수행된 모든 초기화가 B에 의해 보이기 전에 B가 그 물체를 사용한다면(A가 초기화를 완료하지 않았거나 물체의 초기화된 값 중 일부가 B가 사용하는 메모리(캐시 일관성)에 아직 스며들지 않았기 때문에), 프로그램은 충돌할 가능성이 높다.
J2SE 1.4(이전 버전)에서 이중 체크된 잠금을 사용하는 것의 위험성 중 하나는 그것이 작동하는 것처럼 자주 보인다는 것이다: 기법의 정확한 구현과 미묘한 문제가 있는 것을 구별하기가 쉽지 않다.컴파일러, 스케줄러에 의한 스레드의 인터리빙 및 기타 동시 시스템 활동의 특성에 따라 이중 체크된 잠금의 잘못된 구현으로 인한 실패는 간헐적으로만 발생할 수 있다.실패를 재현하는 것은 어려울 수 있다.
J2SE 5.0을 기준으로 이 문제는 해결되었다.휘발성 키워드는 이제 여러 개의 스레드가 싱글톤 인스턴스를 올바르게 처리하도록 한다.이 새로운 관용구는 [3]과 [4]에 설명되어 있다.
// Java 1.5 이상에서 휘발성에 대한 획득/해제 의미론과의 작업 // 휘발성 때문에 Java 1.4 및 이전 의미론에서 깨짐 계급 푸 { 사유의 휘발성이 있는 도우미 조력자; 공중의 도우미 겟헬퍼() { 도우미 localRef = 조력자; 만일 (localRef == 무효의) { 동기화된 (이) { localRef = 조력자; 만일 (localRef == 무효의) { 조력자 = localRef = 새로운 도우미(); } } } 돌아오다 localRef; } // 기타 기능 및 구성원... }
로컬 변수 "을 참조하십시오.localRef", 이것은 불필요해 보인다.그 효과는 도우미가 이미 초기화된 경우(즉 대부분의 경우) 휘발성 필드에 한 번만 액세스("return localRef;"return helper;" 대신 "return localRef;"로 인해), 방법의 전체 성능을 40%까지 향상시킬 수 있다.[7]
Java 9가 소개한 것은VarHandle
보다 어려운 역학적 비용과 순차적 일관성 상실의 희생으로, 여유 있는 원자력을 필드에 접속할 수 있는 클래스(필드 액세스는 더 이상 휘발성 필드에 대한 접속의 글로벌 순서인 동기화 순서에 참여하지 않는, 메모리 접근의 글로벌 순서인 동기화 순서에 참여하지 않음).[8]
// Java 9에 도입된 VarHandles에 대한 획득/해제 의미와 함께 작업 계급 푸 { 사유의 휘발성이 있는 도우미 조력자; 공중의 도우미 겟헬퍼() { 도우미 localRef = 겟헬퍼어콰이어(); 만일 (localRef == 무효의) { 동기화된 (이) { localRef = 겟헬퍼어콰이어(); 만일 (localRef == 무효의) { localRef = 새로운 도우미(); setHelperRelease(localRef); } } } 돌아오다 localRef; } 사유의 정태의 최종의 바핸들 도우미; 사유의 도우미 겟헬퍼어콰이어() { 돌아오다 (도우미) 도우미.getacquire(이); } 사유의 공허하게 하다 setHelperRelease(도우미 가치를 매기다) { 도우미.setRelease(이, 가치를 매기다); } 정태의 { 해보다 { 메서드핸들.찾다 찾다 = 메서드핸들.찾다(); 도우미 = 찾다.findVarHandle(푸.계급, "helper", 도우미.계급); } 잡히다 (반사 작동예외 e) { 던지다 새로운 예외InInInitializerError(e); } } // 기타 기능 및 구성원... }
도우미 객체가 정적인 경우(클래스 로더당 1개) 대안은 주문형 초기화 홀더 숙어[9](앞서 인용한 텍스트의 16.6 목록[10] 참조)이다.
// Java에서 게으른 초기화 수정 계급 푸 { 사유의 정태의 계급 도우미홀더 { 공중의 정태의 최종의 도우미 조력자 = 새로운 도우미(); } 공중의 정태의 도우미 겟헬퍼() { 돌아오다 도우미홀더.조력자; } }
이것은 중첩된 클래스가 참조될 때까지 로드되지 않는다는 사실에 의존한다.
Java 5에서 최종 필드의 의미론을 사용하여 휘발성을 사용하지 않고 도우미 객체를 안전하게 게시할 수 있다.[11]
공중의 계급 파이널워퍼<T> { 공중의 최종의 T 가치를 매기다; 공중의 파이널워퍼(T 가치를 매기다) { 이.가치를 매기다 = 가치를 매기다; } } 공중의 계급 푸 { 사유의 파이널워퍼<도우미> 도우미워퍼; 공중의 도우미 겟헬퍼() { 파이널워퍼<도우미> 온도래퍼 = 도우미워퍼; 만일 (온도래퍼 == 무효의) { 동기화된 (이) { 만일 (도우미워퍼 == 무효의) { 도우미워퍼 = 새로운 파이널워퍼<도우미>(새로운 도우미()); } 온도래퍼 = 도우미워퍼; } } 돌아오다 온도래퍼.가치를 매기다; } }
로컬 변수 tempWrapper는 정확성을 위해 필요하다. null 체크에 helperWrapper를 사용하기만 하면 Java Memory Model에서 허용되는 읽기 순서 변경으로 인해 반환 문이 실패할 수 있다.[12]이 구현의 성과가 반드시 휘발성 구현보다 나은 것은 아니다.
C#의 사용량
이중 체크된 잠금은 에서 효율적으로 구현할 수 있다.NET. 일반적인 사용 패턴은 이중 체크된 잠금을 Singleton 구현에 추가하는 것이다.
공중의 계급 마이싱글턴 { 사유의 정태의 이의를 제기하다 myLock = 새로운 이의를 제기하다(); 사유의 정태의 마이싱글턴 마이싱글턴 = 무효의; 사유의 마이싱글턴() { } 공중의 정태의 마이싱글턴 겟인스턴스() { 만일 (마이싱글턴 이다 무효의) // 첫 번째 검사 { 자물쇠를 채우다 (myLock) { 만일 (마이싱글턴 이다 무효의) // 두 번째(이중) 검사 { 마이싱글턴 = 새로운 마이싱글턴(); } } } 돌아오다 마이싱글턴; } }
이 예에서 "잠금 힌트"는 완전하게 구성되고 사용할 준비가 되었을 때 더 이상 null이 아닌 mySingleton 객체다.
.NET Framework 4.0에서는Lazy<T>
기본적으로 이중 체크된 잠금(ExecutionAndPublic mode)을 사용하여 시공 중에 발생된 예외 또는 전달된 기능의 결과를 저장하는 클래스가 도입되었다.Lazy<T>
:[13]
공중의 계급 마이싱글턴 { 사유의 정태의 읽기 전용 게으름뱅이<마이싱글턴> 마이싱글턴 = 새로운 게으름뱅이<마이싱글턴>(() => 새로운 마이싱글턴()); 사유의 마이싱글턴() { } 공중의 정태의 마이싱글턴 인스턴스 => 마이싱글턴.가치; }
참고 항목
참조
- ^ 슈미트, D 등패턴지향적 소프트웨어 아키텍처 Vol 2, 2000 pp353-363
- ^ a b 데이비드 베이컨 등이중 체크 잠금이 깨졌다"고 선언한다.
- ^ "Support for C++11-14-17 Features (Modern C++)".
- ^ 이중 체크된 잠금이 C++11에서 고정됨
- ^ Boehm, Hans-J (Jun 2005). "Threads cannot be implemented as a library" (PDF). ACM SIGPLAN Notices. 40 (6): 261–268. doi:10.1145/1064978.1065042.
- ^ Haggar, Peter (1 May 2002). "Double-checked locking and the Singleton pattern". IBM.
- ^ Joshua Bloch "유효한 자바, 제3판" 372페이지
- ^ "Chapter 17. Threads and Locks". docs.oracle.com. Retrieved 2018-07-28.
- ^ 브라이언 괴츠 외 연구진Java Concurrency in Practice, 2006 pp348
- ^ Goetz, Brian; et al. "Java Concurrency in Practice – listings on website". Retrieved 21 October 2014.
- ^ [1] Javamemorymodel-토론 메일링 목록
- ^ [2] Manson, Jeremy (2008-12-14). "Date-Race-Ful Lazy Initialization for Performance – Java Concurrency (&c)". Retrieved 3 December 2016.
- ^ Albahari, Joseph (2010). "Threading in C#: Using Threads". C# 4.0 in a Nutshell. O'Reilly Media. ISBN 978-0-596-80095-6.
Lazy<T>
actually implements […] double-checked locking. Double-checked locking performs an additional volatile read to avoid the cost of obtaining a lock if the object is already initialized.
외부 링크
- Jeu George's 블로그에 캡처된 이중 체크 잠금 메커니즘의 문제
- Portland Pattern Repository의 "Double Checked Locking" 설명
- Portland Pattern Repository의 "Double Checked Locking is breaked" 설명
- 스콧 마이어스와 안드레이 알렉산드레스쿠의 종이 "C++와 이중 체크 잠금의 위험"(475KB)
- 기사 "이중 체크 잠금: Brian Gotz의 "영리한, 그러나 깨진"
- 기사 "경고! Alen Holub의 멀티프로세서 세계에서의 스레딩"
- 이중 체크 잠금 및 Singleton 패턴
- 싱글톤 패턴 및 나사산 안전
- VC++ 2005의 휘발성 키워드
- Java 이중 체크 잠금 솔루션의 예 및 타이밍
- "More Effective Java With Google's Joshua Bloch".