이중 체크 잠금

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] 이 방법을 호출할 때마다 잠금 장치를 획득하고 해제하는 오버헤드는 불필요해 보인다. 초기화가 완료되면 잠금 장치를 획득하고 해제하는 것은 불필요해 보인다.많은 프로그래머들은 다음과 같은 방법으로 이 상황을 최적화하려고 시도했다.

  1. (잠금을 얻지 않고) 변수가 초기화되었는지 확인하십시오.초기화된 경우 즉시 반환하십시오.
  2. 자물쇠를 따다.
  3. 변수가 이미 초기화되었는지 다시 확인하십시오. 다른 스레드가 먼저 잠금을 획득한 경우 초기화를 이미 수행했을 수 있음.그렇다면 초기화된 변수를 반환하십시오.
  4. 그렇지 않으면 변수를 초기화하고 반환하십시오.
// 다중 스레드 버전 깨짐 // "이중 체크 잠금" 숙어 계급  {     사유의 도우미 조력자;     공중의 도우미 겟헬퍼() {         만일 (조력자 == 무효의) {             동기화된 () {                 만일 (조력자 == 무효의) {                     조력자 = 새로운 도우미();                 }             }         }         돌아오다 조력자;     }      // 기타 기능 및 구성원... } 

직관적으로 이 알고리즘은 문제에 대한 효율적인 해결책처럼 보인다.그러나 이 기술은 많은 미묘한 문제를 가지고 있으므로 대개 피해야 한다.예를 들어 다음과 같은 이벤트 순서를 고려하십시오.

  1. 스레드 A는 값이 초기화되지 않았음을 인지하여 잠금을 얻고 값을 초기화하기 시작한다.
  2. 일부 프로그래밍 언어의 의미론 때문에 컴파일러에서 생성된 코드는 A가 초기화 수행을 마치기 전에 부분적으로 구성된 객체를 가리키도록 공유 변수를 업데이트할 수 있다.예를 들어, Java에서 생성자에 대한 호출을 인라인 처리한 경우, 스토리지가 할당되고 인라인 처리된 생성자가 객체를 초기화하기 전에 공유 변수가 즉시 업데이트될 수 있다.[6]
  3. 스레드 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]

공중의 계급 마이싱글턴 {     사유의 정태의 읽기 전용 게으름뱅이<마이싱글턴> 마이싱글턴 = 새로운 게으름뱅이<마이싱글턴>(() => 새로운 마이싱글턴());      사유의 마이싱글턴() { }      공중의 정태의 마이싱글턴 인스턴스 => 마이싱글턴.가치; } 

참고 항목

참조

  1. ^ 슈미트, D 등패턴지향적 소프트웨어 아키텍처 Vol 2, 2000 pp353-363
  2. ^ a b 데이비드 베이컨 등이중 체크 잠금이 깨졌다"선언한다.
  3. ^ "Support for C++11-14-17 Features (Modern C++)".
  4. ^ 이중 체크된 잠금이 C++11에서 고정됨
  5. ^ 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.
  6. ^ Haggar, Peter (1 May 2002). "Double-checked locking and the Singleton pattern". IBM.
  7. ^ Joshua Bloch "유효한 자바, 제3판" 372페이지
  8. ^ "Chapter 17. Threads and Locks". docs.oracle.com. Retrieved 2018-07-28.
  9. ^ 브라이언 괴츠 외 연구진Java Concurrency in Practice, 2006 pp348
  10. ^ Goetz, Brian; et al. "Java Concurrency in Practice – listings on website". Retrieved 21 October 2014.
  11. ^ [1] Javamemorymodel-토론 메일링 목록
  12. ^ [2] Manson, Jeremy (2008-12-14). "Date-Race-Ful Lazy Initialization for Performance – Java Concurrency (&c)". Retrieved 3 December 2016.
  13. ^ 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.

외부 링크