2011-08-28 7 views
11

멀티 프로세서 컴퓨터에서 메모리 캐시 일관성 문제를 해결하는 MSDN 문서, "Synchronization and Multiprocessor Issues"을 방금 읽었습니다. 그들이 제공하는 예제에 경쟁 조건이있을 수 있다고 생각하지 않았기 때문에 이것은 정말로 나에게 열려 있습니다. 이 기사에서는 메모리에 쓰기가 실제로 내 코드에 쓰여진 순서대로 (다른 CPU의 관점에서) 발생할 수 없다고 설명합니다. 이것은 나에게 새로운 개념입니다! 여러 CPU에 걸쳐 일관성을 캐시해야 할 변수에서 "휘발성"키워드를 사용중요 섹션이있는 Delphi에서 캐시 일관성 문제 방지?

  1. :

    이 문서에서는이 개 솔루션을 제공합니다. 이것은 C/C++ 키워드이며 Delphi에서 사용할 수 없습니다.

  2. InterlockExchange() 및 InterlockCompareExchange() 사용. 필자가해야한다면 델파이에서 할 수있는 일입니다. 조금 지저분 해 보인다.

이 기사에서는 "다음과 같은 동기화 기능이 메모리 정렬을 보장하는 데 필요한 적절한 장벽을 사용합니다. • 중요한 섹션을 시작하거나 종료하는 기능"을 언급합니다.

이것은 내가 이해하지 못하는 부분입니다. 이것은 중요한 섹션을 사용하는 함수로 제한되는 메모리에 대한 모든 쓰기가 캐시 일관성 및 메모리 순서 문제로부터 영향을받지 않는다는 것을 의미합니까? Interlock *() 함수에 대해서는 아무 것도 없습니다.하지만 툴 벨트에있는 다른 도구는 유용 할 것입니다!

답변

7

우선 언어 표준에 따르면 휘발성은 기사에서 말하는대로하지 않습니다. 휘발성의 취득 및 릴리스 의미는 MSVC에 고유합니다. 다른 컴파일러 또는 다른 플랫폼에서 컴파일하면 문제가 될 수 있습니다. C++ 11에서는 언어 지원 원자 변수를 소개합니다.이 변수는 결과적으로 마침내 스레딩 구성 요소로서의 휘발성 변수의 사용을 마침내 막을 수 있습니다.

중요한 섹션과 뮤텍스가 실제로 구현되어 있으므로 보호 된 변수의 읽기 및 쓰기가 모든 스레드에서 올바르게 표시됩니다.

중요한 섹션과 뮤텍스 (잠금)를 생각하는 가장 좋은 방법은 직렬화를 가져 오는 장치라는 것입니다. 즉, 이러한 잠금에 의해 보호되는 코드 블록은 겹치지 않고 순차적으로 실행됩니다. 직렬화는 메모리 액세스에도 적용됩니다. 캐시 일관성 또는 읽기/쓰기 순서 재 지정으로 인한 문제는 없습니다.

연동 기능은 메모리 버스에서 하드웨어 기반 잠금을 사용하여 구현됩니다. 이 함수는 잠금없는 알고리즘에서 사용됩니다. 이것이 의미하는 바는 크리티컬 섹션과 같은 중량 잠금을 사용하지 않고 경량 하드웨어 잠금을 사용한다는 것입니다.

잠금없는 알고리즘은 잠금 기반 알고리즘보다 더 효율적일 수 있지만 잠금없는 알고리즘은 올바르게 작성하는 것이 훨씬 더 어려울 수 있습니다. 성능 영향을 식별 할 수없는 경우 잠금을 해제하는 것보다 중요한 섹션을 선호하십시오.

읽을 가치가있는 또 다른 기사는 The "Double-Checked Locking is Broken" Declaration입니다.

+1

David, 부울, 정수 등의 참조는 원자 적으로 (적절하게 정렬 된 경우) 따라서 스레드로부터 안전하다고 종종 언급됩니다. 나는 받아 들여진 응답이 여기에서 생각한다 http://stackoverflow.com/questions/510031/list-of-delphi-data-types-with-atomic-read-write-operations 관점에서 이것을 둔다. "읽기는 스레드로부터 안전합니다. 쓰기는 스레드로부터 안전하지 않습니다." –

+0

@LU RD 정확해야합니다. threadsafe는 무엇을 의미합니까? 그리고 정확하게 "읽기는 스레드로부터 안전합니다, 쓰기는 스레드로부터 안전하지 않습니다"라는 의미입니까? –

+0

모든 스레드 컨텍스트에서 사용하기에 안전한 원자 변수에 대한 작업을 고려할 수 있다고 가정하지 않는 것은 일반적인 경고 일뿐입니다. –

7

이 MSDN 문서는 멀티 스레드 응용 프로그램 개발의 첫 번째 단계에 불과합니다. 즉, 읽기/쓰기가 가능한 데이터가 확실하지 않기 때문에 "공유 변수를 잠금으로 보호합니다 (중요한 섹션이라고도 함)를 의미합니다. 모든 스레드에 대해 동일 "합니다.

CPU 당 코어 캐시는 잘못된 값을 읽게되는 가능한 문제 중 하나 일뿐입니다.경쟁 조건으로 이어질 수있는 또 다른 문제는 리소스에 동시에 쓰는 두 개의 스레드입니다. 이후에 저장할 값을 아는 것은 불가능합니다.

코드는 데이터가 일관성이 있기를 기대하기 때문에 일부 멀티 스레드 프로그램은 잘못 작동 할 수 있습니다. 멀티 스레딩을 사용하면 작성한 코드가 개별 명령어를 통해 공유 변수를 처리 할 때 예상대로 실행되는지 확신 할 수 없습니다.

InterlockedExchange/InterlockedIncrement 함수는 실제로 모든 CPU 코어에 대한 캐시 일관성을 강제하고, 따라서 ASM 동작 코드의 실행 스레드 안전 할 것이다 저수준의 ASM 로크 프리픽스 오피 코드 (또는 XCHG EDX,[EAX] 오피 같이 설계된 잠긴) . 예를 들어

, 여기 당신이 문자열 값을 할당 할 때 문자열 참조 횟수가 구현되는 방법이다 (System.pas에 _LStrAsg 참조 -이 our optimized version of the RTL for Delphi 7/2002에서입니다 - 델파이 원래의 코드가 저작권 때문에) :

  MOV  ECX,[EDX-skew].StrRec.refCnt 
      INC  ECX { thread-unsafe increment ECX = reference count } 
      JG  @@1 { ECX=-1 -> literal string -> jump not taken } 
      ..... 
     @@1: LOCK INC [EDX-skew].StrRec.refCnt { ATOMIC increment of reference count } 
      MOV  ECX,[EAX] 
      ... 

첫 번째 INC ECXLOCK INC [EDX-skew].StrRec.refCnt 사이에는 차이가 있습니다. 첫 번째 증분 ECX 및 참조 횟수 변수가 아니지만 첫 번째는 스레드로부터 안전하지 않지만 두 번째 접두사는 LOCK로 시작되므로 스레드로부터 안전합니다.

그러나이 LOCK 접두어는 multi-thread scaling in the RTL의 문제 중 하나입니다. 최신 CPU에서는 더 좋지만 여전히 완벽하지는 않습니다. 따라서 응용 프로그램이 더 나은 규모 및 전체 전력의 사용을 만들 것입니다,

var GlobalVariable: string; 
    GlobalSection: TRTLCriticalSection; 

procedure TThreadOne.Execute; 
var LocalVariable: string; 
begin 
    ... 
    EnterCriticalSection(GlobalSection); 
    LocalVariable := GlobalVariable+'a'; { modify GlobalVariable } 
    GlobalVariable := LocalVariable; 
    LeaveCriticalSection(GlobalSection); 
    .... 
end; 

procedure TThreadTwp.Execute; 
var LocalVariable: string; 
begin 
    ... 
    EnterCriticalSection(GlobalSection); 
    LocalVariable := GlobalVariable; { thread-safe read GlobalVariable } 
    LeaveCriticalSection(GlobalSection); 
    .... 
end; 

지역 변수를 사용하여 임계 영역을 짧게한다 :

그래서 중요한 섹션을 사용하여 스레드 안전 코드를 만드는 가장 쉬운 방법입니다 귀하의 CPU 코어 중. EnterCriticalSectionLeaveCriticalSection 사이에는 하나의 스레드 만 실행됩니다. 다른 스레드는 EnterCriticalSection 호출에서 대기합니다 ... 따라서 중요한 섹션이 짧을수록 응용 프로그램이 빠릅니다. 일부 잘못 설계된 멀티 스레드 응용 프로그램은 실제로 모노 스레드 응용 프로그램보다 느려질 수 있습니다!

중요한 섹션 내의 코드가 예외를 발생시킬 수있는 경우 잠금 해제를 보호하고 응용 프로그램의 deadlock을 방지하려면 항상 try ... finally LeaveCriticalSection() end; 블록을 작성해야합니다.

공유 데이터를 잠금 (예 : 중요 섹션)으로 보호하면 델파이는 스레드로부터 안전합니다. RTL 함수 내부에 LOCK이 있어도 참조 카운팅 된 변수 (문자열 등)조차도 보호해야한다는 점에 유의하십시오.이 LOCK은 올바른 참조 카운팅을 가정하고 메모리 누수를 피할 수 있지만 스레드로부터 안전하지는 않습니다 . 되도록 빨리하기 위해 see this SO question.

InterlockExchangeInterlockCompareExchange의 목적은 공유 포인터 변수 값을 변경하는 것입니다. 포인터 값에 액세스하기 위해 임계 섹션의 "경량"버전으로 볼 수 있습니다.

모든 경우에 멀티 스레드 코드를 작성하는 것은 쉽지 않습니다. 심지어 , as a Delphi expert just wrote in his blog입니다.

공유 데이터가없는 간단한 스레드를 작성하거나 (스레드가 시작되기 전에 데이터의 개인 복사본을 만들거나 읽기 전용 공유 데이터를 사용합니다 (에센스별로 스레드 안전) 또는 일부를 잘 호출해야합니다 설계되고 검증 된 라이브러리 - 예 : http://otl.17slon.com -을 사용하면 많은 디버깅 시간을 절약 할 수 있습니다.

+0

팁 주셔서 감사합니다. 공유 메모리를 피하고 작업 할 사본을 만드는 것에 대한 팁. InterlockExchange가 어떻게 성능을 향상시킬 수 있는지도 알 수 있습니다. 난 그냥 성급하게 최적화하고 싶지 않아. 중요한 섹션에서 코드를 읽기 쉽도록 만들면 거기에서 시작해야합니다. Interlock *()을 사용하는 MSDN 기사의 예제가 작동하지만 혼란 스럽습니다. 나는 너의 연결을 지금 체크 아웃하기 위하여 갈 것이다. – Troy