소프트웨어 기반 해결책이 최신 컴퓨터 아키텍쳐에서 작동하지 않을 수 있음 > 하드웨어적 해결 필요
🥑 Memory Barrier
메모리 베리어, 메모리 장벽
메모리 모델: 컴퓨터 아키텍처가 응용 프로그램에게 메모리를 어떻게 제공하는가에 대한 모델
- 강한 순서, strongly ordered: 한 프로세서의 메모리 변경 결과가 다른 몯느 프로세서에 즉시 보임
- 약한 순서, weakly ordered: 한 프로세서의 메모리 변경 결과가 다른 프로세서에 즉시 보이지 않음
컴퓨터 아키텍처는 메모리의 모든 변경 사항을 다른 모든 프로세서로 전파하는 명령어 제공
- 다른 프로세서에서 실행 중인 스레드에 메모리 변경 사항이 보이는 것을 보장함
- 메모리 장벽/펜스: 메모리 장벽 명령어가 실행될 때, 시스템은 다음 명령어가 실행되기 전에 모든 적재/저장이 완료되도록 함
- 메모리 장벽 명령어 위로 선언된 모든 문장들이 실행되어야 그 다음 문장이 실행됨
메모리 베리어 종류
1. Load barrier: 메모리에서 데이터를 읽어오는 명령어 다음에 실행되는 명령어들을 지연
데이터 읽기가 완료되기 전에 실행된 명령어로부터 발생하는 문제 해결
- Store barrier: 메모리에 데이터를 쓰는 명령어 이전에 실행되는 명령어들을 지연
데이터 쓰기가 완료되기 전에 실행된 명령어로부터 발생하는 문제 해결
메모리 베리어 없이는: 원자적 연산이 제대로 작동안할 수도 있음
- 스레드/프로세스 간 변수 접근 순서가 보장 안될 수 있음
- 메모리 베리어로 스레드/프로세스 간 변수 접근 순서 제어 가능
🥑 Hardware instructions
하드웨어 명령어
한 워드의 내용을 원자적으로 검사하고 변경하거나 두 워드의 내용을 원자적으로 교환할 수 있는 명령어
- 원자적: 인터럽트 되지 않는 하나의 단위로 실행
test_and_set()
명령어
- 명령어가 원자적으로 실행됨
- 다른 코어에서 이 명령어가 동시에 실행된다면 이들은 임의의 순서로 순차적으로 실행됨
- lock이 가능한지 현재 lock 상태를 반환하고 lock을 true로 설정
- 예) 두 프로세스를 동시 실행할 때
- 처음 실행하는 프로세스는
lock = false
이므로 임계구역에 진입 가능
- 임계구역에서 진입하면서
lock = true
로 바꿔줌
- 두번째 프로세스는
lock = true
라서 임계구역 진입 불가
- 상호 배제 만족
compare_and_swap()
명령어
- 두 워드의 내용 교환에 기반을 둔 두 개의 워드에 대해 원자적인 연산 수행
int compare_and_swap(int *value, int expected, int new_value) {
int temp = *value;
if (*value == expected)
*value = new_value;
return temp;
}
- lock이 가능한지 현재 lock 상태를 반환하고
value==expected
이면 value를 new value
값으로 설정
- lock의 초기값은 0
- 여러 프로세스가 동시에 실행될 때:
- 첫 프로세스는
lock = 0
이므로 임계구역에 진입 가능
- 임계구역에 진입하면서
lock = 1
로 바꿈
- 나머지 프로세스는
lock = 1
이라 임계구역에 진입 불가
- 첫 프로세스가 임계구역을 떠나면서
lock = 0
으로 바꿈
- 다른 프로세스 진입 가능
- 상호 배제 조건 만족
🥑 volatile 키워드
volatile
- 자바 코드의 변수를 메인 메모리에 저장할 것을 명시하기 위해 쓰임
- 모든 volatile 변수는 컴퓨터의 메인 메모리로부터 읽힘
- volatile 변수에 대한 쓰기 작업은 메인 메모리로 직접 이루어짐
- CPU 캐시가 쓰이지 않음
- 최적화 대상 아님
- 최적화: 불필요한 코드 없애기, 성능이 느린 저장영역(메모리 등)에 대한 접근 줄이기, 코드의 순서 변경하기
- (예) 컴파일러는 변수 재정의 등 무의미한 코드는 알아서 삭제함
- volatile 변수는 변수 대입 작업 전부 실행
- 외부 요인에 의해 변수 값이 변경될 가능성이 있는 변수에는 volatile을 꼭 써야 함
- 외부 요인: memory-mapped I/O, 인터럽트 서비스 루틴, 멀티 쓰레드 환경 등
Java 5부터 Happens-Before 보장
- 한 쓰레드가 volatile 변수를 수정할 때, 이 쓰레드가 volatile 변수를 수정하기 전에 수정한 모든 변수들이 함께 메인 메모리에 저장(flush)됨
- 한 쓰레드가 volatile 변수를 메인 메모리에서 읽어들일 때, volatile 변수를 수정하면서 메인 메모리에 함께 저장된 다른 모든 변수들도 함께 읽여들여짐
volatile의 남발은 성능에 악영향
- 컴파일러 단위에서 수행 가능한 최적화 막힘
- 해당 변수를 사용하는 모든 위치에서 레지스터 값을 읽어도되는 상황까지 모두 메모리/캐쉬에서 읽도록 함
필요한 위치에 memory barrier를 사용한다면 굳이 volatile 사용할 필요 없음
🥑 멀티코어 환경에서 동기화
멀티코어 프로세세어세는 여러 코어가 동시에 공유 메모리에 접근할 수 있음
-> 공유 자원에 대한 동시 접근을 제어하고 일관성을 유지하기 위한 동기화 기법이 필요함
-> 멀티코어 환경에서 동기화를 이뤄지게 하는 여러 가지 방법 존재
Lock 사용
- 여러 개의 스레드가 동시에 접근하는 경우에 발생할 수 있는 데이터 불일치 문제 방지
- 뮤텍스, 세마포어 등
원자적 연산
여러 코어가 동시에 수행하는 연산 간의 데이터 경쟁 발생 방지
동시 접근 제어
하드웨어적 락: 메모리 베리어
메모리 베리어를 사용하면 캐시와 메인 메모리 간의 데이터를 동기화시켜 캐시와 메모리 간의 일관성을 유지 & 동시성 문제 해결
- 프로세서는 메인 메모리에서 데이터를 읽어와 캐시에 저장
- 캐시에 저장된 데이터는 메인 메모리와 불일치할 수 있음
- 코어1에서 메모리의 데이터를 읽어와 캐시에 저장
- 코어 2에서 동일한 메모리의 데이터 변경
- 다시 메인 메모리에 저장
- 코어1의 캐시에는 변경된 데이터가 반영되지 않은 채로 남아있을 수 있음
- 메모리 베리어는 이러한 캐시와 메인 메모리의 일관성 유지를 위해 사용
메모리 베리어를 사용하면 코어가 메모리에 대한 연산을 완료하기 전, 다른 코어가 동일한 메모리에 접근하는 것을 방지함
락 없는 동기화 방법
원자적 연산/메모리 베리어 등 활용
병렬 프로그래밍 라이브러리 및 프레임워크
병렬 프로그래밍을 지원하는 라이브러리/프레임워크 사용
참고: