Chapter 08 - 사용자 모드 에서 의 스 레 드 동기 화

Cache Lines
       다 중 핵 에서 효율 적 인 프로그램 을 만 들 려 면 Cache Lines 를 이해 할 필요 가 있 습 니 다. '운영 체제' 를 배 웠 을 때 CPU 가 물리 적 메모리 에서 내용 을 읽 을 때 한 바이트 씩 읽 는 것 이 아니 라 여러 바이트 의 데 이 터 를 읽 어 Cache Line 에 넣 는 것 을 잘 알 고 있 을 것 입 니 다.하나의 Cache Line 은 32, 64 또는 128 개의 바이트 (한 마디 로 2 의 지수) 일 수 있 으 며, 일반적으로 32, 64 또는 128 바이트 수 에 따라 정렬 된다.
       주의해 야 할 것 은 다 중 핵 에 있 는 Cache Line 에서 메모리 업 데 이 트 를 할 때 문제 가 발생 할 수 있 습 니 다. 다음 인 스 턴 스 를 보십시오.
1.       CPU 1 은 메모리 의 한 바 이 트 를 읽 는 김 에 근처에 있 는 몇 개의 바 이 트 를 Cache Line 에 같이 읽 습 니 다.
2.       CPU 2 는 CPU 1 과 같은 바 이 트 를 읽 고 이 바이트 근처에 있 는 몇 개의 바 이 트 를 읽 었 습 니 다.
3.       CPU 1 은 Cache Line 에 있 는 이 바이트 의 내용 을 바 꾸 었 으 나 RAM 에 제때 업데이트 되 지 않 았 습 니 다.
4.       CPU 2 는 이 바이트 의 내용 을 읽 습 니 다.CPU 2 의 Cache Line 에 이 바이트 가 있 기 때문에 메모리 에 읽 지 않 아 데이터 가 동기 화 되 지 않 습 니 다.
       위 에서 언급 한 상황 은 어떤 상황 에서 재난 적 인 결 과 를 초래 할 수 있다.그러나 칩 제조 자 는 이미 이 문 제 를 의식 하고 있 으 며, 현재 다 핵 시스템 에서 어떤 Cache Line 이 수정 되면 다른 CPU 에 Cache Line 에 대응 하 는 내용 을 검사 하 라 고 통지 할 것 이다.CPU 1 이 수 정 된 내용 을 RAM 에 기록 하도록 강제 한 다음 CPU 2 가 RAM 으로 가서 내용 을 읽 고 해당 하 는 Cache Line 을 다시 채 웁 니 다.알다 시 피 한편, Cache Line 은 성능 을 향상 시 킬 수 있 습 니 다.다른 한편 으로 는 성능 을 떨 어 뜨 릴 수도 있다.
       한 마디 로 성능 을 향상 시 키 려 면 모든 데이터 구 조 를 Cache Line 에 모 아야 한다.서로 다른 CPU 가 서로 다른 메모리 주소 에 접근 할 때 같은 Cache Line 접근 에 분포 되 지 않도록 하 는 것 이 목표 입 니 다.
       structCUSTINFO {
                            DWORDdwCustomerID; // Mostly read-only
                            intnBalanceDue; // Read-write
                            wchar_tszName[100]; // Mostly read-only
                            FILETIMEftLastOrderDate; // Read-write
       };
       CPU 의 Cache Line 을 가 져 오 는 방법 은 GetLogicalProcessor Information 함 수 를 호출 하 는 것 입 니 다. 이 함 수 는 SYSTEM LOGICAL PROCESSOR INFORMATION 구조 체 를 되 돌려 주 고 구조 체 내의 Cache 도 메 인 을 통 해 CACHE DESCRIPTOR 구조 체 를 가리 키 며 이 구조 체 내 에 도 메 인 이 있 습 니 다. 이 도 메 인 값 이 있 으 면 다음 과 같은 비슷 한 코드 로 프로그램 을 최적화 할 수 있 습 니 다.
      
#define CACHE_ALIGN 64

       //Force each structure to be in a different cache line.
       struct__declspec(align(CACHE_ALIGN)) CUSTINFO {
              DWORDdwCustomerID; // Mostly read-only
              wchar_tszName[100]; // Mostly read-only

              //Force the following members to be in a different cache line.
              __declspec(align(CACHE_ALIGN))
              intnBalanceDue; // Read-write

              FILETIMEftLastOrderDate; // Read-write

       }

 
고급 스 레 드 동기 화 - 중요 섹 션
       스 레 드 가 사용 가능 한 자원 을 기다 리 고 있 을 때 CPU 자원 을 낭비 하지 않 아 도 되 는 메커니즘 이 필요 합 니 다.
       하나의 스 레 드 가 공유 자원 에 접근 하거나 특정한 이벤트 알림 을 기다 리 려 면 스 레 드 는 시스템 함 수 를 호출 하고 표지 스 레 드 가 기다 리 고 있 는 인 자 를 전달 해 야 합 니 다.
       만약 에 시스템 이 자원 이 사용 가능 하거나 특수 사건 이 발생 한 것 을 감지 하면 함수 가 되 돌아 오고 스 레 드 를 예약 가능 한 상태 로 만 듭 니 다. 만약 에 자원 이 사용 되 지 않 거나 특수 사건 이 발생 하지 않 으 면 시스템 은 스 레 드 를 대기 상태 에 두 고 예약 할 수 없 게 합 니 다 (그러면 CPU 자원 을 낭비 하지 않 습 니 다).
       다음 코드 를 보십시오.
     
const int COUNT = 1000;
         int g_nSum = 0;
         DWORD WINAPI FirstThread(PVOID pvParam){
                   g_nSum = 0;
                   for (int n = 1; n <=COUNT; n++) {
                            g_nSum += n;
                   }
                   return(g_nSum);
         }

         DWORD WINAPI SecondThread(PVOIDpvParam) {
                   g_nSum = 0;
                   for (int n = 1; n <=COUNT; n++) {
                            g_nSum += n;
                   }
                   return(g_nSum);
              }

       이 두 스 레 드 가 단독으로 실행 되면 모든 것 이 정상 입 니 다. First Thread 실행 과정 에서 CPU 시간 이 다 되 고 SecondThread 가 CPU 에 배 치 돼 실 행 될 것 이 라 고 생각 합 니 다. 이것 은 g nSum 의 값 이 어 지 럽 고 마지막 으로 g nSum 값 은 알 수 없습니다. 이것 은 프로그램 프로 그래 밍 의 취지 와 어 긋 납 니 다.
       윈도 우 는 이러한 문 제 를 해결 하 는 방법 인 CRITICAL SECTION 구조 체 에 대응 하 는 API 함수 몇 개 를 제공 합 니 다.
       1.VOID InitializeCriticalSection(PCRITICAL_SECTION pcs)
       이 함 수 는 CRITICAL SECTION 구조 체 의 초기 화 를 제공 하고 일부 구성원 변수의 값 을 설정 합 니 다. 이 함 수 는 EnterCritical Section 전에 호출 되 어야 합 니 다.
       2.VOID EnterCriticalSection(PCRITICAL_SECTIONpcs)
       이 함 수 는 구조 체 의 일부 구성원 변 수 를 검사 합 니 다. 이 변 수 는 어떤 스 레 드 가 자원 에 접근 하고 있 는 지 표시 합 니 다. 이 함 수 는 검 사 를 했 습 니 다.
a)       스 레 드 가 자원 에 접근 하지 않 으 면 EnterCritical Section 함수 가 구성원 변 수 를 업데이트 하여 스 레 드 가 자원 에 접근 할 수 있 는 권한 을 수 여 받 았 음 을 표시 하고 즉시 돌아 와 스 레 드 가 계속 실 행 될 수 있 도록 합 니 다.
b)       구조 체 구성원 변수 가 호출 스 레 드 가 자원 에 접근 할 수 있 는 권한 을 부여 했다 면 EnterCritical Section 함수 업데이트 변 수 는 호출 스 레 드 가 권한 을 수 여 받 은 방문 횟수 를 표시 하고 스 레 드 를 즉시 되 돌려 줍 니 다.
c)       구조 체 변수 가 다른 스 레 드 에 접근 할 수 있 는 권한 이 있 는 자원 을 표시 하면 EnterCritical Section 함 수 는 스 레 드 를 대기 목록 에 넣 습 니 다. 그러면 스 레 드 는 CPU 자원 을 낭비 하지 않 습 니 다. 자원 이 방출 되면 이 스 레 드 는 예약 가능 한 상태 로 CPU 의 호출 이 실 행 될 때 까지 기 다 립 니 다.
       EnterCritical Section 함수 에 걸 린 스 레 드 는 장기 적 으로 스케줄 링 이 불가능 한 상태 일 수 있 습 니 다. 이 상 태 를 굶 어 죽 는 것 이 라 고 할 수 있 습 니 다. 그러나 실제로 이런 상황 은 일어나 지 않 습 니 다. EnterCritical Section 은 결국 시간 초과 로 인해 이상 을 일 으 킬 수 있 기 때 문 입 니 다.
       3. BOOLTryEnterCriticalSection(PCRITICAL_SECTIONpcs)
       EnterCritical Section 함수 와 달리 TryEnterCritical Section 은 호출 된 스 레 드 가 자원 에 접근 할 수 있 는 지 여 부 를 반환 값 으로 표시 합 니 다. 자원 이 다른 스 레 드 에 점용 되 었 을 때 FALSE 로 돌아 갑 니 다. 그렇지 않 으 면 TRUE 로 돌아 갑 니 다.
       이 함 수 를 이용 하여 스 레 드 는 자원 에 접근 할 수 있 는 지 여 부 를 빠르게 확인 할 수 있 습 니 다. 그렇지 않 으 면 간단하게 기다 리 는 것 이 아니 라 다른 일 을 계속 할 수 있 습 니 다. TryEnterCritical Section 함수 가 TRUE 로 돌아 오 면 CRITICAL SECTION 구조 체 의 구성원 변 수 를 업데이트 하여 현재 스 레 드 가 자원 에 접근 하고 있 음 을 표시 합 니 다. 따라서 각 TryEnterCritical Section 은 TRUE 로 돌아 간 후 LeaveCrit 를 다시 호출 해 야 합 니 다.icalSection。
       4. VOIDLeaveCriticalSection(PCRITICAL_SECTION pcs)
       이 함 수 는 CRITICAL SECTION 구조 체 내의 구성원 변 수 를 검사 하고 현재 스 레 드 가 자원 에 접근 할 수 있 는 권한 을 부 여 받 은 횟수 를 표시 하 는 계수 기 를 1 로 줄 입 니 다. 카운터 가 0 이상 이면 함 수 는 간단하게 되 돌아 오 면 됩 니 다. 카운터 가 0 이면 구조 체 내의 구성원 변 수 를 변경 하여 스 레 드 가 자원 을 차지 하지 않 았 음 을 표시 하고 EnterCritical Sectio 를 호출 하 는 스 레 드 가 있 는 지 확인 합 니 다.n. 스 레 드 를 하나 더 선택 하면 예약 가능 한 상태 입 니 다.
Critical Sections and Spinlocks
       한 스 레 드 가 다른 스 레 드 가 사용 하 는 임계 영역 에 들 어 가 려 고 시도 하면 현재 스 레 드 는 즉시 대기 상태 에 있 습 니 다. 이 는 스 레 드 가 사용자 모드 (User mode) 에서 커 널 모드 (Kernelmode) 로 바 뀌 어야 한 다 는 것 을 의미 합 니 다. (약 1000 CPU 주기)이러한 전환 의 대 가 는 매우 비싸다. 다 중 핵 시스템 에서 현재 스 레 드 가 사용자 모드 에서 커 널 모드 로 전환 되 기 전에 자원 을 점용 하 는 스 레 드 는 이미 자원 을 방출 했 을 것 이다. 이런 상황 에서 대량의 CPU 시간 이 낭비 되 었 다.
       임계 구역 의 성능 을 향상 시 키 기 위해 마이크로 소프트 는 자전거 자물쇠 (spinlock) 를 도입 했다. EnterCritical Section 함수 가 호출 될 때 자전거 자 물 쇠 를 순환 적 으로 사용 하여 일정 횟수 내 에 자원 을 얻 으 려 고 시도 했다. 모든 시도 가 실 패 했 을 때 만 라인 은 내부 핵 모드 에 들 어가 대기 상태 에 있다.
       두 개의 관련 함수:
       1.BOOLInitializeCriticalSectionAndSpinCount(
                                          PCRITICAL_SECTION pcs,
                                          DWORD dwSpinCount);
       첫 번 째 매개 변 수 는 Initialize Critical Section 의 매개 변수 와 마찬가지 로 두 번 째 매개 변 수 는 스 레 드 자동 잠 금 시도 횟수 를 나타 낸다.
       2.DWORD SetCriticalSectionSpinCount(
                                          PCRITICAL_SECTION pcs,
                                          DWORD dwSpinCount);
       이 함 수 는 자동 잠 금 시도 횟수 를 바 꿉 니 다.
       PS: dwSpinCount 의 추천 치 는 4000 입 니 다.
Slim Reader-Writer Locks
       이 시리즈 함 수 는 전형 적 인 독자 - 작성 자 문제 에서 비롯 되 었 습 니 다. 한 번 에 여러 명의 독자 가 읽 을 수 있 지만 한 번 에 한 명의 작성 자 만 쓸 수 있 습 니 다. Critical Section (임계 구역) 과 달리 어떤 스 레 드 가 데 이 터 를 간단하게 읽 는 지, 어떤 스 레 드 가 데 이 터 를 수정 하 는 지, 여러 스 레 드 가 동시에 데 이 터 를 읽 을 수 있 도록 합 니 다.
       이 시 리 즈 는 하나의 구조 체 와 세 개의 해당 하 는 함수 가 있다.
typedef struct_RTL_SRWLOCK {

PVOID Ptr;

} RTL_SRWLOCK, *PRTL_SRWLOCK;

VOID InitializeSRWLock(PSRWLOCK SRWLock);

VOID AcquireSRWLockShared(PSRWLOCK SRWLock);

VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);

VOID WINAPI AcquireSRWLockExclusive(  __inout  PSRWLOCK SRWLock);

VOID WINAPI ReleaseSRWLockExclusive(  __inout  PSRWLOCK SRWLock);
       InitialSRWLock 함 수 는 구조 체 를 초기 화 합 니 다.
       AcquireSRQLock Shared 와 ReleaseSRWLock Shared 함수 의 기능 은 독자 권한 을 얻 고 방출 하 는 것 입 니 다.
       AcquireSRW LockExclusive 와 ReleaseSRW LockExclusive 함 수 는 작성 자 권한 을 가 져 오고 방출 하 는 것 입 니 다.
Volatile read &Volatile write:
       가장 간단 하고 직접적 으로 데 이 터 를 공유 하 는 방법 입 니 다. 프로 그래 밍 으로 이 루어 지 는 것 이 아니 라 volatile 키 워드 를 이용 하여 컴 파일 러 에 게 이 키워드 가 수식 한 데 이 터 를 임시 복사 하지 말 라 고 알려 주 고 업데이트 후 바로 메모리 에 다시 써 야 합 니 다.       간단하게 예 를 들 어 설명 합 니 다. CPU 0 의 Cache 와 CPU 1 의 Cache 는 메모리 에서 volatile 로 수 정 된 내용 을 동시에 액세스 합 니 다. CPU 0 이 데 이 터 를 변경 할 때 변 경 된 데 이 터 를 메모리 에 업데이트 한 다음 CPU 1 에 이 데 이 터 를 다시 읽 으 라 고 알 립 니 다. volatile 수식 이 없 으 면 CPU 0 과 CPU 1 의 Cache 데이터 가 일치 하지 않 습 니 다. 이것 은 분명히 우리 가 보기 싫 은 것 입 니 다.       PS: Volatile 은 일반 데이터 형식 외 에 struct 와 class 를 수식 할 수 있 습 니 다.
 
interlocked APIs
     Interlocked API 시 리 즈 는 다음 과 같 습 니 다. 이 시 리 즈 는 모든 첫 번 째 매개 변수 가 지정 한 메모리 주소 의 데 이 터 를 원자 조작 으로 실행 하도록 보장 합 니 다. 구체 적 인 함 수 를 사용 하면 더 이상 군말 하지 않 고 MSDN 을 찾 아 볼 수 있 습 니 다.
LONG InterlockedExchangeAdd( 
PLONG volatile plAddend, 
LONG lIncrement); 
LONGLONG InterlockedExchangeAdd64( 
PLONGLONG volatile pllAddend, 
LONGLONG llIncrement);
LONG InterlockedExchange(
PLONG volatile plTarget,
LONG lValue);
LONGLONG InterlockedExchange64(
PLONGLONG volatile plTarget,
LONGLONG lValue);
PVOID InterlockedExchangePointer(
PVOID* volatile ppvTarget,
PVOID pvValue);
PVOID InterlockedCompareExchange( 
PLONG plDestination,
LONG lExchange, 
LONG lComparand); 
PVOID InterlockedCompareExchangePointer( 
PVOID* ppvDestination, 
PVOID pvExchange, 
PVOID pvComparand);
LONGLONG InterlockedCompareExchange64( 
LONGLONG pllDestination, 
LONGLONG llExchange, 
LONGLONG llComparand);
LONG InterlockedIncrement(PLONG plAddend); 
LONG InterlockedDecrement(PLONG plAddend);

좋은 웹페이지 즐겨찾기