메모리와 동시성 (Memory and Concurrency)
공유 메모리와 동시성 문제
공유 메모리의 개념
- 공유 상태는 공유 메모리에 존재합니다.
- 공유 메모리는 동시성 문제를 일으킬 수 있습니다.
- 메모리가 어떻게 공유되는지 이해하는 것이 중요합니다.
공유 메모리의 유형
- 동일 프로세스 내의 동일 스레드가 다른 시간에 사용하는 메모리
- 동일 프로세스 내의 다른 스레드가 사용하는 메모리
- 다른 프로세스가 사용하는 메모리
공유 메모리 획득
프로세스 내에서 공유 메모리
- 프로세스 내에서 메모리를 공유하는 것은 특별한 설정이 필요하지 않음.
프로세스 간 공유 메모리
- 커널의 지원이 필요하며, 다음과 같은 방법들이 있습니다:
shm_open(): 이름이 지정된 공유 메모리 영역에 연결.
- 메모리 매핑된 파일을 사용.
- 분기(fork) 전에 공유 매핑 생성.
일관성 문제
race condition 레이스 조건 조심
- 레이스 조건(race condition)은 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제입니다.
- 동기화 메커니즘을 사용하여 이러한 문제를 방지할 수 있습니다.
일관성 문제 개요
- 메모리와 동시성의 문제는 주로 일관성 문제와 관련이 있습니다.
- 전용 컴퓨터 모델에서는 다음과 같은 기대가 있습니다:
- 메모리 위치에 쓰기 작업이 즉시 반영됨.
- 메모리 위치에 대한 쓰기는 지속적임.
동시성 흐름에서의 일관성 문제
- 이러한 기대는 동시성 흐름에서 깨질 수 있습니다.
- 동기화를 통해 이를 완화할 수 있습니다.
- 그러나 동기화는 타이밍뿐만 아니라 더 많은 것을 제어해야 합니다.
동기화 메커니즘
- 완전한 동기화를 위해 하드웨어 및 시스템 수준에서 보장해야 합니다.
- 캐시 일관성을 유지하기 위해 메모리 배리어(memory barrier)가 필요합니다.
- 동시 프로그래밍에서는 쓰기의 즉각성과 내구성에 대한 기대가 깨질 수 있으므로 동기화가 필수적입니다.
시간적 동기화 및 공간적 동기화
- 동기화는 단순히 타이밍에 관한 것이 아니라, 작업이 시스템 전체에 표시되도록 보장하는 것입니다.
- 시간적 고려 사항: 연산 o1이 o2보다 먼저 실행되고, 인터럽트 없이 유지됨.
- 공간적 고려 사항: 한 연산이 시스템의 다른 부분에서도 일관되게 보이는 상태를 유지함.
시간적 동기화 (Temporal Synchronization)
시간적 동기화 개념
- 지금까지 동기화를 시간적 구조로 생각했습니다:
- 연산 o1이 연산 o2 전에 발생함.
- 연산 순서가 중단되지 않음.
공간적 동기화 (Spatial Synchronization)
- 공간적 동기화도 고려해야 합니다:
- 한 연산이 시스템의 다른 부분에서 볼 수 있는 상태가 됨.
캐싱 (Caching)
캐싱 개요
- 현대 컴퓨터에는 여러 계층의 캐시가 있습니다.
- 일부 캐시는 공유되고, 일부는 로컬입니다:
- 특정 CPU 코어에 로컬
- 코어의 하위 집합에 로컬
- 프로세스에 로컬
캐싱의 이유
- 성능 향상을 위해 캐시가 사용됩니다.
- 낮은 레벨의 캐시는 훨씬 빠르지만 훨씬 작습니다.
- L0-L1 캐시는 코어에 로컬, L2-3는 코어 또는 코어의 하위 집합에 로컬.
- L4는 일반적으로 공유됨.
캐싱의 구조 및 중요성
- 최신 컴퓨터는 성능 향상을 위해 여러 계층의 캐싱(L0-L4)을 사용합니다.
- 캐시 레벨이 높아질수록(예: L0 -> L4) 시간이 더 많이 걸립니다.
- 로컬 캐시에 대한 쓰기는 다른 코어에 즉시 표시되지 않을 수 있습니다.
- 각 캐시 레벨은 다음 레벨의 블록을 저장합니다.
- 블록 위치와 크기는 레벨마다 다를 수 있습니다.
- 읽기는 원하는 데이터를 가진 첫 번째 레벨에서 가져옵니다.
- 쓰기는 결국 모든 레벨에 전파됩니다.
쓰기 전파 (Write Propagation)
쓰기 전파 문제
- 쓰기가 모든 레벨에 전파되는 것은 시간이 걸립니다.
- 로컬 캐시에 쓰인 데이터는 다른 코어에서 보이지 않을 수 있습니다.
- 예를 들어, 레지스터는 특정 코어에서만 볼 수 있습니다.
- 쓰기가 모든 캐시 수준에 즉시 전파되지 않아 동시 읽기 시 불일치가 발생할 수 있습니다.
- 이를 해결하기 위해 메모리 배리어가 필요합니다.
쓰기 전파 시나리오
- 코어 C0가 메모리 위치 m에 쓰기 작업을 실행.
- 쓰기 작업이 C0의 L1 캐시에 저장됨.
- 코어 C1이 메모리 위치 m을 읽기 작업을 실행.
- m이 C1의 L1 또는 L2에 없음.
- C1이 공유 L3에서 m을 읽음.
- C0의 L1이 m을 C0의 L2로 전파.
- C0의 L2가 m을 공유 L3로 전파.
메모리 장벽 (Memory Barriers)
메모리 장벽 예시
mfence (x86-64)
dmb (ARM)
- 메모리 장벽은 코어 전체에 대한 쓰기 가시성을 보장합니다.
메모리 장벽 개념
- 메모리 장벽은 메모리의 가시성을 보장하기 위한 하드웨어 기능입니다.
- 메모리 장벽은 다음을 수행할 수 있습니다:
- 현재 코어를 블록하여 모든 코어에서 쓰기를 볼 수 있도록 함.
- 모든 쓰기를 볼 수 있을 때까지 현재 코어를 블록.
- 모든 코어가 쓰기를 볼 수 있을 때까지 특정 위치에 접근하는 것을 블록.
- CPU 명령어 재정렬이 이 명령어에 영향을 미치지 않도록 방지.
메모리 장벽을 사용한 쓰기 전파
- 코어 C0가 메모리 위치 m에 쓰기 작업을 실행.
- 쓰기 작업이 C0의 L1 캐시에 저장됨.
- 코어 C1이 m에 대한 모든 쓰기를 위한 장벽을 설정.
- 코어 C1이 m에 대한 읽기 작업을 실행.
- C1이 m을 읽기 전에 블록됨.
- C0의 L1이 m을 C0의 L2로 전파.
- C0의 L2가 m을 공유 L3로 전파.
- C1이 공유 L3에서 m을 읽음.
동기화와 메모리 장벽
-
많은 POSIX 동기화 함수(예: fork(), pthread_mutex_lock(), pthread_mutex_unlock())는 올바른 작동을 보장하기 위해 메모리 장벽을 사용합니다.
-
동기화 원시 기능은 메모리 장벽을 사용합니다.
-
예를 들어, 다음 함수들은 모두 장벽을 포함합니다:
fork()
pthread_mutex_lock()
pthread_mutex_unlock()
pthread_create()
pthread_join()
- 기본적으로 모든 POSIX 동기화 함수.
공유 메모리의 여러 방법
암시적으로 공유되는 메모리
명시적으로 공유되는 메모리
- 프로세스는 명시적으로 메모리 공유를 요청할 수 있습니다.
- 이 메모리는 변경 가능하며, 변경 사항은 프로세스 간에 볼 수 있습니다.
- 커널은 공유 메모리를 설정합니다.
- POSIX 시스템은 명시적 메모리 공유를 위한 두 가지 기본 시스템 호출과 세 가지 방법을 제공합니다:
mmap(): 파일을 메모리에 매핑하고, 파일에 대한 변경 사항을 프로세스 간에 공유.
mmap()을 사용하여 부모와 자식 프로세스 간에 익명 공유 매핑 생성.
shm_open(): 이름이 지정된 공유 메모리 영역을 엽니다.
mmap() 시스템 호출
mmap() 개요
mmap()은 메모리 매핑 도구의 다용도 도구입니다.
- 커널에 프로세스 가상 메모리 맵을 조작하도록 요청합니다.
- 파일을 메모리에 매핑하거나, 익명 메모리 매핑을 생성할 수 있습니다.
- 대응하는 함수는
munmap()입니다.
- 올바르게 사용하는 것은 매우 복잡할 수 있습니다.
mmap() 사용 예시
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
- 필수 인수는
flags와 fd입니다.
flags 인수는 생성할 매핑 유형을 결정합니다:
MAP_PRIVATE 또는 MAP_SHARED 중 하나를 포함해야 합니다.
MAP_ANONYMOUS는 파일을 매핑하지 않음을 의미합니다.
addr 인수는 매핑을 배치할 가상 메모리 맵의 위치를 지정합니다:
- 종종 0으로 지정하여 커널이 결정하게 합니다.
fd 인수는 다음 중 하나여야 합니다:
len은 파일의 몇 바이트를 매핑할지를 결정합니다.
- 파일을 매핑하는 경우,
offset은 매핑할 파일의 첫 번째 바이트를 결정합니다.
mmap() 예시
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
return 1;
}
size_t len = 1024;
void *mapped = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
close(fd);
return 1;
}
munmap(mapped, len);
close(fd);
return 0;
}
shm_open() 시스템 호출
shm_open
() 개요
shm_open()은 이름이 지정된 공유 메모리 영역을 엽니다.
- 공유 메모리는 프로세스 간에 공유될 수 있으며, 커널에 의해 설정됩니다.
shm_open() 사용 예시
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = shm_open("/my_shared_mem", O_CREAT | O_RDWR, 0666);
if (fd == -1) {
return 1;
}
size_t len = 1024;
ftruncate(fd, len);
void *shared_mem = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared_mem == MAP_FAILED) {
close(fd);
return 1;
}
munmap(shared_mem, len);
close(fd);
shm_unlink("/my_shared_mem");
return 0;
}
요약
- 캐싱과 CPU 아키텍처는 시간적 동기화 이상의 것이 필요합니다.
- 메모리 장벽은 데이터 가시성을 보장합니다.
- 메모리 장벽은 하드웨어 기능입니다.
- 캐시는 메인 RAM보다 훨씬 빠릅니다.
- POSIX 동기화 원시 기능은 메모리 장벽을 사용합니다.
- 공유 메모리는 커널 지원이 필요합니다.
- 파일은 메모리에 매핑될 수 있습니다.