다음을 통해 공유


동기화 및 다중 프로세서 문제

애플리케이션은 단일 프로세서 시스템에서만 유효한 가정으로 인해 다중 프로세서 시스템에서 실행할 때 문제가 발생할 수 있습니다.

스레드 우선 순위

다른 스레드보다 우선 순위가 높은 두 개의 스레드가 있는 프로그램을 고려합니다. 단일 프로세서 시스템에서 우선 순위가 높은 스레드는 스케줄러가 우선 순위가 높은 스레드에 대한 기본 설정을 제공하기 때문에 우선 순위가 높은 스레드에 대한 제어를 포기하지 않습니다. 다중 프로세서 시스템에서는 두 스레드를 각각 자체 프로세서에서 동시에 실행할 수 있습니다.

애플리케이션은 경합 조건을 방지하기 위해 데이터 구조에 대한 액세스를 동기화해야 합니다. 우선 순위가 높은 스레드가 우선 순위가 낮은 스레드의 간섭 없이 실행된다고 가정하는 코드는 다중 프로세서 시스템에서 실패합니다.

메모리 순서 지정

프로세서가 메모리 위치에 쓰면 성능 향상을 위해 값이 캐시됩니다. 마찬가지로 프로세서는 성능을 향상시키기 위해 캐시의 읽기 요청을 충족하려고 시도합니다. 또한 프로세서는 애플리케이션에서 요청하기 전에 메모리에서 값을 가져오기 시작합니다. 이는 투기적 실행의 일부 또는 캐시 라인 문제로 인해 발생할 수 있습니다.

CPU 캐시는 병렬로 액세스할 수 있는 은행으로 분할할 수 있습니다. 즉, 메모리 작업을 순서대로 완료할 수 있습니다. 메모리 작업이 순서대로 완료되도록 하기 위해 대부분의 프로세서는 메모리 장벽 지침을 제공합니다. 전체 메모리 장벽은 메모리 장벽 명령 앞에 나타나는 메모리 읽기 및 쓰기 작업이 메모리 장벽 명령 후에 나타나는 메모리 읽기 및 쓰기 작업 전에 메모리에 커밋되도록 합니다. 읽기 메모리 장벽메모리 읽기 작업만 정렬하고 쓰기 메모리 장벽은 메모리 쓰기 작업만 정렬합니다. 또한 이러한 지침은 컴파일러가 장벽을 넘어 메모리 작업을 다시 정렬할 수 있는 최적화를 사용하지 않도록 설정하도록 합니다.

프로세서는 획득, 해제 및 펜스 의미 체계를 사용하여 메모리 장벽에 대한 지침을 지원할 수 있습니다. 이러한 의미 체계는 작업 결과를 사용할 수 있게 되는 순서를 설명합니다. 획득 의미 체계를 사용하면 코드에서 그 후에 나타나는 작업의 결과 전에 작업 결과를 사용할 수 있습니다. 릴리스 의미 체계를 사용하면 코드에서 작업 앞에 나타나는 작업의 결과 후에 작업 결과를 사용할 수 있습니다. 펜스 의미 체계는 획득 및 릴리스 의미 체계를 결합합니다. 펜스 의미 체계가 있는 작업의 결과는 코드에서 그 뒤와 그 앞에 나타나는 작업의 작업 이후에 나타나는 작업의 결과 앞에 사용할 수 있습니다.

SSE2를 지원하는 x86 및 x64 프로세서에서 지침은 mfence(메모리 펜스), lfence(로드 펜스) 및 스펜스(저장소 펜스)입니다. ARM 프로세서에서 침입은 dmbdsb입니다. 자세한 내용은 프로세서에 대한 설명서를 참조하세요.

다음 동기화 함수는 적절한 장벽을 사용하여 메모리 순서를 보장합니다.

  • 중요한 섹션을 입력하거나 나가는 함수
  • SRW 잠금을 획득하거나 해제하는 함수
  • 일회성 초기화 시작 및 완료
  • EnterSynchronizationBarrier 함수
  • 동기화 개체를 신호로 표시하는 함수
  • 대기 함수
  • 연동 함수(NoFence 접미사가 있는 함수 또는 _nf 접미사가 있는 내장 함수 제외)

경합 상태 수정

다음 코드에는 다중 프로세서 시스템에 경합 조건이 있습니다. 처음 실행되는 CacheComputedValue 프로세서는 기본 메모리에 쓰기 전에 기본 메모리에 쓸 fValueHasBeenComputediValue 수 있기 때문입니다. 따라서 동시에 실행되는 FetchComputedValue 두 번째 프로세서는 TRUEfValueHasBeenComputed 지만 새 값 iValue 은 여전히 첫 번째 프로세서의 캐시에 있으며 메모리에 기록되지 않았습니다.

int iValue;
BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (!fValueHasBeenComputed) 
  {
    iValue = ComputeValue();
    fValueHasBeenComputed = TRUE;
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (fValueHasBeenComputed) 
  {
    *piResult = iValue;
    return TRUE;
  } 

  else return FALSE;
}

위의 경합 상태는 휘발성 키워드(keyword) 또는 InterlockedExchange 함수를 사용하여 값을 TRUE로 설정하기 전에 모든 프로세서에 iValue 대해 값 fValueHasBeenComputed 이 업데이트되도록 하여 복구할 수 있습니다.

Visual Studio 2005부터 /volatile:ms 모드로 컴파일된 경우 컴파일러는 휘발성 변수에 대한 읽기 작업에 대한 의미 체계를 획득하고( CPU에서 지원하는 경우) 휘발성 변수에 대한 쓰기 작업에 대한 의미 체계를 해제합니다. 따라서 다음과 같이 예제를 수정할 수 있습니다.

volatile int iValue;
volatile BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (!fValueHasBeenComputed) 
  {
    iValue = ComputeValue();
    fValueHasBeenComputed = TRUE;
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (fValueHasBeenComputed) 
  {
    *piResult = iValue;
    return TRUE;
  } 

  else return FALSE;
}

Visual Studio 2003을 사용하면 휘발성에서 휘발성 참조로의 순서가 지정됩니다. 컴파일러는 휘발성 변수 액세스의 순서를 다시 지정하지 않습니다. 그러나 이러한 작업은 프로세서에서 다시 정렬할 수 있습니다. 따라서 다음과 같이 예제를 수정할 수 있습니다.

int iValue;
BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (InterlockedCompareExchange((LONG*)&fValueHasBeenComputed, 
          FALSE, FALSE)==FALSE) 
  {
    InterlockedExchange ((LONG*)&iValue, (LONG)ComputeValue());
    InterlockedExchange ((LONG*)&fValueHasBeenComputed, TRUE);
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (InterlockedCompareExchange((LONG*)&fValueHasBeenComputed, 
          TRUE, TRUE)==TRUE) 
  {
    InterlockedExchange((LONG*)piResult, (LONG)iValue);
    return TRUE;
  } 

  else return FALSE;
}

중요 섹션 개체

Interlocked 변수 액세스

Wait 함수