유저 모드 스레드 동기화
■ 윈도우는 모든 스레드가 상호 통신 없이 각자의 작업을 수행할 때 최고의 성능을 발휘 -> 하지만 스레드가 독립적으로 상호 통신 없이 독립적으로 수행되는 경우는 거의 없음
▶ 상호 통신 없이 독립적으로 수행?
○ Process A에서는 File을 읽고 있음과 동시에 Process B에서는 File을 쓰는 작업을 실행 -> 수정 중인 데이터를 읽는 행위 -> 매우 불안정한 데이터
■ 스레드 상호 통신을 수행해야 하는 상황
1. 다수의 스레드가 공유 리소스에 접근하며 리소스가 손상되지 않아야 할 때
2. 어떤 스레드가 하나 혹은 다수의 스레드에게 작업이 완료 되었음을 알려야 할 때
■ 스레드 동기화 중에선 제일 빠름 -> 최상의 성능
■ 프로세스 내의 스레드들 사이에서만 동기화 수행 가능
원자적 접근
■ 원자적 접근 -> 스레드가 특정 리소스를 접근할 때 다른 스레드는 동일 시간에 동일 리소스에 접근할 수 없는 것
윈도우는 선점형 운영체제이기 때문에 어떤 스레드가 수행 중에 언제든 다른 스레드가 수행될 수 있음을 명시하도록
■ CPU 플랫폼마다 서로 상이한 동작원리 -> x86 계열의 CPU라면 인터락 함수들은 버스에 하드웨어 시그널을 실어서 다른 CPU가 동일 메모리 주소에 접근하지 못하도록 함
int a = 0;
int WINAPI Thread1(PVOID pvParam) {
a++;
return(0);
}
int WINAPI Thread2(PVOID pvParam) {
a++;
return(0);
}
▶ 위의 코드로 두개의 스레드를 생성했을 경우 결과 값을 예상 -> int a ==2라고 생각할 수 있지만 정답은 "모른다" -> 위에 설명했듯이 윈도우는 선점형 멀티스레드 운영체제이기 때문에 스레드는 수행 중에 언제든 다른 스레드로 제어를 빼앗길 수 있음 -> 컴파일러에 따라, 설치된 CPU, 갯수에 따라 다 결과값은 다다름 -> 중간에 인터럽트(방해) 받지 않고 값을 "원자적으로" 변경할수 있는 방법이 필요
관련 함수
LONG InterlockedExchangeAdd (
PLONG volatile plAddend,
LONG lIncrement
);
▶ 단순히 두 번째 매개 변수로 음수 값을 전달하기만 하면 값이 감소됨
LONG InterlockedExchange (
PLONG volatile plTarget,
LONG lValue
);
LONGLONG InterlockedExchange64 (
PLONGLONG volatile plTarget,
LONGLONG lValue
);
PVOID InterlockedExchange64 (
PVOID* volatile ppvTarget,
PVOID pvValue
);
▶ 첫 번째 매개변수로 전달되는 주소가 담고있는 값을 두 번째 매개변수로 전달되는 값을 원자적으로 변경하는 함수들
캐시라인
■ 캐시라인 -> 캐시영역의 기본단위, 지역성에 근거하면 CPU가 데이터를 로드할 때, 해당 데이터의 주변 데이터도 가까운 시간 안에 다시 참조 될 가능성이 높음 -> 이러한 공간적 지역성에 근거해 캐시는 필요 데이터 로딩 시, 그 주변 데이터들까지 함께 로딩 하는데 이때 로딩되는 단위를 캐시블록 또는 캐시라인이라고 함
▶ 성능 향상을 위해 존재함 -> 분명 성능 향상에 도움이 되지만 멀티 프로세서 머신에서는 거꾸로 성능을 저해하는 요인이 될 수도 있음
■ 멀티 프로세서에서 CPU가 캐시 라인에 있는 정보를 변경하면 다른 CPU는 이러한 사실을 알아채고 자신의 캐시 라인에 있는 정보를 무효화 시킴 -> 여러 스레드가 하나의 리소스에 접근 했을때 발생하는 문제와 비슷한 문제에 대한 해결방안
■ 위와 같은 특성 때문에 애플리케이션이 사용하는 데이터는 캐시 라인의 크기와 그 경계 단위로 묶어서 다루는 것이 좋음 -> 적어도 하나 이상의 캐시라인 경계로 분리된 서로 다른 메모리 블록에 각각의 CPU가 독립적으로 접근하는 것을 보장할 수 있게 된다
▶ 읽기 전용의 데이터와 읽고 쓰는 데이터를 분리하는 것이 좋음
▶ 동일한 시간에 접근하는 데이터들을 묶어서 구성하는 것이 좋음
BOOL GetLogicalProcessorInformation(
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION Buffer,
PDWORD ReturnedLength
);
▶ CPU의 캐시 라인 크기를 얻어오는 가장 쉬운 방법
○ 얻어온 값으로 __declspec(align(CACHE_SIZE))명령어로 캐시 기능 개선 가능
크리티컬 섹션
■ 크리티컬 섹션 -> 공유 리소스에 대해 베타적으로 접근해야하는 작은 코드의 집합
변수 단위가 아닌 코드를 "원자적으로" 수행하기 위한 방법
■ 서로 다른 프로세스에 존재하는 스레드 사이의 동기화에는 사용 불가
CRITICAL_SECTION NameOfStruct
▶ 크리티컬섹션 사용 시 필요한 구조체
▶ 프로세스 내의 모든 스레드가 접근할 수 있도록 전역변수로 선언하는 것이 일반적
▶ 구조체 자체가 문서화되어 있지 않는 것은 아님 -> 단지 마이크로소프트가 이 구조체의 내용에 대해 사용자가 알 필요가 없다 생각해 구조체의 내용을 공개하지 않음
void InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
▶ CRITICAL_SECTION 구조체를 초기화 함 -> 해당 구조체 사용하기전(EnterCriticalSection) 호출 필수
void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
▶ EnterCriticalSection과 LeaveCriticalSection으로 공유 리소스를 작성하는 코드를 감싼다
▶ EnterCirticalSection -> 스레드가 공유 리소스에 접근함을 알림
○ 만일 공유 리소스를 사용하는 스레드가 없다면 CRITICAL_SECTIOIN 구조체 내의 멤버변수를 갱신하여 이 함수를 호출한 스레드가 공유 자원에 대한 접근 권한을 획득했음을 설정한 후 스레드가 계속 수행될 수 있도록 지체 없이 반환 -> 반환 == 코드 실행
○ 이미 한번 호출 후 다시 두번째 호출했을 경우 접근 권한 획득을 위해 이 함수를 몇 번 호출하였는지 멤버 변수에 기록 후 바로 함수 반환
○ CRITICAL_SECTION 내의 멤버변수를 확인해 보았을 때 다른 스레드가 이미 공유 리소스에 대한 권한을 획득한 상태라면 해당 함수는 이벤트 커널 오브젝트를 이용해 대기상태로 만듬
멀티 프로세서 머신에서 두 개의 스레드가 완벽하게 동일한 시점에 해당 함수를 호출하는 경우 ○ 단 하나의 스레드만이 공유 리소스에 대한 접근 권한을 획득한 후 다른 스레드는 대기 상태로 전환한다 -> 이 경우 스레드는 아주 오랫동안 스케줄될 수 없게 됨
▶ LeaveCriticalSection -> 스레드가 공유 리소스 접근을 끝냈음을 알림
○ 해당 함수 호출시 자동으로 CRITICAL_SECTION 멤버변수를 갱신해 대기중인 스레드를 스케줄 가능 상태로 만듬
○ CRITICAL_SECTION 구조체 내의 멤버변수를 확인하고, 공유 리소스에 대해 접근 권한을 획득한 횟수 1만큼 감소 시킴 -> 감소 후 0보다 크면 아무런 작업없이 반환 -> 0이 되면 공유 리소스에 대한 접근 권한을 획득한 스레드가 없는 것으로 멤버를 갱신하고, 이전에 EnterCriticalSection을 호출해 대기 상태로 진입한 스레드가 있는지 확인 -> LeaveCriticalSection은 이 함수를 호출한 스레드를 절대로 대기 상태로 빠뜨리지 않고 바로 반환됨
▶ 해당 함수들 호출 시에 인자는 CRITICAL_SECTION 구조체의 주소를 전달
▶ 내부적으로 인터락 함수를 사용해 매우 빠르게 동작하나 서로 다른 프로세스에 존재하는 스레드 사이의 동기화에는 사용할 수 없다는 치명적 단점 존재
void DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
▶ 프로세스가 더 이상 공유 리소스를 사용할 필요가 없으면, 해당 함수를 호출해 CRITICAL_SECTION을 삭제해야 함
BOOL TryEnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
▶ TryEnterCriticalSection -> EnterCriticalSection 대신 사용 가능
○ 함수를 호출한 스레드를 절대 대기 상태로 진입시키지 않음 -> 대신 함수의 반환 값으로 리소스에 대한 접근 권한을 얻었는지의 여부를 가져옴 -> TRUE로 반환 되는 경우 CRTICAL_SECTION의 멤버변수를 현재 스레드가 공유 리소스의 접근 권한을 획득한 것으로 갱신하게 됨
■ 다른 스레드가 이미 진입한 크리티컬 섹션에 특정 스레드가 재진입을 시도하면, 스레드는 바로 대기 상태로 변경됨 -> 스레드가 유저 모드에서 커널 모드로 전환(많은 리소스 필요, 무거운 작업)
■ 멀티 프로세서 머신의 경우 현재 공유 리소스를 소유하고 있는 스레드가 해당 공유 리소스에 대한 작업을 빨리 반환할 가능성 또한 존재 -> 커널 모드로 전환을 완료 하기전에 다시 유저모드로 돌아와야 할 수 있음 -> 리소스 낭비, CPU 시간 낭비
BOOL InitializeCriticalSectionAndSpinCount(
LPCRITICAL_SECTION lpCriticalSection,
DWORD dwSpinCount
);
▶ 크리티컬 섹션의 성능 개선(바로 위와 같은 예로 인한 시간 낭비)을 위한 스핀락 메커니즘EnterCriticalsSection이 호출되면 일정 회수 동안 스핀락을 사용하여 리소스 획득을 시도하는 루프를 수행 -> 스핀락을 수행하는 동안 공유 리소스에 대한 획득에 실패한 경우에만 스레드를 대기 상태로 전환하기 위해 커널 모드로의 전환을 시도하도록 한다
▶ 단일 프로세스를 가진 머신에서는 스핀락을 사용하는 것이 좋지 않으므로 해당 함수를 단일 프로세서를 가진 머신에서 호출하게 되면 dwSpinCount 매개변수로 전달한 값은 무시되며 0으로 설정된다
▶ 크리티컬 섹션을 사용할 때 항상 이함수를 사용하는걸 추천함
DWORD SetCriticalSectionSpinCount(
LPCRITICAL_SECTION lpCriticalSection,
DWORD dwSpinCount
);
▶ 크리티컬 섹션의 스핀 횟수를 변경할 수 있는 함수
슬림 리더-라이터 락
■ 크리티컬 섹션과 유사하나 SRWLock의 경우 리소스의 값을 읽기만 하는 리더(스레드)들과 그 값을 수정하려는 라이터(스레드)들이 완전히 구분되어 있을 경우에만 사용할 수 있다 -> 공유된 리소스를 읽기만 하는 리더들은 동시에 여러 스레드가 접근하더라도 리소스의 값을 손상시키지 않기 때문에 동시에 수행되어도 무방한 특징을 이용 -> 라이터가 리소스의 내용을 수정하는 동안에는 어떠한 리더, 라이터도 공유 리소스에 접근해선 안된다
■ SRWLOCK 오브젝트를 삭제하거나 파괴하는 함수는 존재하지 않으며, 이러한 작업은 시스템이 자동으로 수행해줌
■ SRWLOCK 오브젝트를 반복적으로 획득할 수 없다
void InitializeSRWLock(
PSRWLOCK SRWLock
);
▶ 해당 함수를 이용해 SRWLOCK 구조체를 할당하고 초기화
▶ 사용되는 SRWLOCK 구조체는 실제로 이 포인터가 무엇을 가리키는지 문서화되어 있지 않기 때문에, 이 멤버를 직접 사용할 수는 없다
void AcquireSRWLockExclusive(
PSRWLOCK SRWLock
);
void ReleaseSRWLockExclusive(
PSRWLOCK SRWLock
);
▶ AcquireSRWLockExclusive -> 라이터가 SRWLock을 이용해 보호하려는 공유 리서스에 대한 배타적인 접근 권한을 획득하기 위해 해당 함수를 사용
▶ ReleaseSRWLockExclusive -> 라이터가 공유 리소스 사용 후 락을 해제하기 위해 호출하는 함수
void AcquireSRWLockShared(
PSRWLOCK SRWLock
);
void ReleaseSRWLockShared(
PSRWLOCK SRWLock
);
▶ 리더가 사용하는 함수들
조건변수
■ 프로세스간 공유 불가능한 유저 모드 오브젝트
■ 조건변수를 사용하면 스레드가 리소스에 대한 락을 해제하고 SleepConditionVariableCS나 SleepConditionVariableSRW 함수에서 지정한 상태가 될 때까지 스레드를 블로킹 해준다
void InitializeConditionVariable(
PCONDITION_VARIABLE ConditionVariable
);
▶ 조건 변수를 초기화 해주는 함수
BOOL SleepConditionVariableCS(
PCONDITION_VARIABLE ConditionVariable,
PCRITICAL_SECTION CriticalSection,
DWORD dwMilliseconds
);
BOOL SleepConditionVariableSRW(
PCONDITION_VARIABLE ConditionVariable,
PSRWLOCK SRWLock,
DWORD dwMilliseconds,
ULONG Flags
);
▶ CriticalSection, SRW에 따른 함수들
void WakeConditionVariable(
PCONDITION_VARIABLE ConditionVariable
);
void WakeAllConditionVariable(
PCONDITION_VARIABLE ConditionVariable
);
▶ SleepConditionVariable*류 함수들로 블로킹 된 스레드를 다시 실행시키는 함수
각 유저모드 스레드 동기화 속도 비교
1. Interlock*류 함수
▶ CPU가 배타적으로 메모리에 접근할 수 있도록 락을 설정하기 때문에 일반적인 읽기/쓰기에 비해 느림
▶ 락을 설정한다 -> 결국 특정 시간에 단일의 CPU만 메모리에 접근할 수 있도록 허용한다는 것을 의미
2. SRWLock
▶ 크리티컬 섹션의 성능 결과와 비교적 유사하나 크리티컬 섹션보다 조금 더 성능이 뛰어나다는 것을 확인할 수 있음 -> 크리티컬 섹션 대신 SRWLock이 크리티컬 섹션보다 조금 더 성능이 뛰어나다는것이 확인 가능
3. 크리티컬 섹션
▶ 크리티컬 섹션에 대한 경쟁이 발생하면 성능은 더욱더 나빠지게 됨
▶ SRWLock와 다르게 읽기 작업또한 한번에 하나씩 밖에 접근 못하므로 SRWLock보다 느림