[CS 스터디] 4주차 동기화, 메모리
동기화
동기화란, 여러 프로세스 혹은 스레드에서 하나의 공유 자원에 접근 할 때, 접근 순서를 보장해주는 방식이 동기화이다.
동기화가 필요한 이유
여러 프로세스 혹은 스레드가 하나의 자원에 접근할 때, Race condition이 발생하게 되는데, 이를 해결하기 위해서는 동기화가 필요하다.
Race condition (경쟁 상태)
Race condition은 여러 프로세스 혹은 스레드가 하나의 자원에 동시에 접근할 때, 서로 자원을 선점하려 경쟁하는 것을 뜻한다.
이 과정에서 실행마다 순서가 달라지게 되고, 이는 일관되지 못한 결과를 만들어낸다
import kotlin.concurrent.thread
var counter = 0
fun main() {
val threads = List(1000) {
thread {
counter++
}
}
threads.forEach { it.join() }
println("counter = $counter")
}
위 코드는 스레드 1000개를 생성하고, 각 스레드에서 counter 변수에 1씩 추가하는 코드이다.
의도한 바는, counter가 1000이 되는 것이지만, 실제로 counter는 1000을 반환하지 않는다.
이유는 간단하다.
1000개의 스레드가 순차적으로 실행 (counter++) 되는 것이 아니고, 동시에 counter++를 호출하기 때문이다.
그렇기 때문에, 어떤 연산은 무시되는 효과가 생기는 것이다.
이를 해결하기 위한 대표적인 동기화 기법이 Mutex와 Semaphore이다.
Mutex
Mutex는 하나의 자원에 대해 동기화하는 기법이다.
Mutex는 공유 자원을 사용할 때 Lock을 걸고, 사용이 끝나면 Lock을 해제하는 방식으로 작동한다.
하나의 프로세스가 공유 자원을 사용하고 있으면, 자원에 Lock이 걸리기 때문에 다른 프로세스는 자원에 접근할 수 없는 구조이다.
그렇다면, 위 Race condition 코드에 Mutex를 적용하면 어떻게 될까
Java의 synchronized를 적용하였다.
import kotlin.concurrent.thread
var counter = 0
val lock = Any()
fun main() {
val threads = List(1000) {
thread {
synchronized(lock) {
counter++
}
}
}
threads.forEach { it.join() }
println("counter = $counter")
}
이전의 코드와 다르게, 이 예시는 정상적인 결과값이 나오게 된다.
다만, Mutex는 반드시 Lock을 건 프로세스에서만 Lock을 해제할 수 있기 때문에, 이 점을 유의해야 한다.
그렇지 않을 경우, Deadlock이 발생하게 된다. (Kotlin의 Coroutine에서 synchronized를 사용하지 않는 이유가 여기에 있다)
Semaphore
하나의 공유에 대한 Race condition을 해결하는 Mutex와 달리, Semaphore는 n개의 자원에 대한 Race condition을 해결하는 기법이다.
정수 변수로 나타내지며, 아래와 같이 두가지 연산을 가진다.
P 연산: 작업을 하기 전 호출되며, Semaphore의 값을 1 감소시킨다. 만약 0일 경우, 프로세스는 대기하게 된다.
V 연산: 작업을 완료한 이후 호출된다. Semaphore의 값을 1 증가시킨다.
주차장을 예로 들면 이해가 쉽다.
먼저 주차장에 10개의 자리가 있다고 가정한다.
1번부터 10번까지의 차가 들어오면, P 연산을 호출해 남은 자리를 감소시킨다.
11번 차가 들어왔을 때, 남은 자리가 0이기 때문에 대기를 하게 된다.
이때, 1번 차가 주차장을 나가면, V 연산을 호출함과 동시에 남은 자리가 1 늘어나고, 11번 차가 그 자리에 들어갈 수 있게 된다.
Deadlock (교착상태)
Deadlock은 프로세스가 자원을 획득하지 못하고, 무한정 대기에 빠지는 것을 말한다.
아래 4가지 조건이 만족되면 Deadlock이 발생할 수 있다.
상호배제: 한번에 하나의 프로세스만 자원을 사용할 수 있다.
점유대기: 최소 하나의 자원을 점유하고 있으며, 다른 프로세스에서 추가적인 자원을 점유하기 위해 대기하고 있어야 한다.
비선점: 다른 프로세스의 자원을 작업이 끝날때 까지 빼앗을 수 없다.
순환대기: 프로세스의 집합에서 순환 형태로 자원을 대기하고 있어야 한다.
Deadlock 해결 방법
Deadlock 해결방법은 아래와 같이 3가지가 있다.
Deadlock prevention: Deadlock을 예방하기 위한 방법으로, Deadlock 발생 조건 중 하나를 제거한다.
Deadlock avoidance: Deadlock을 회피하기 위한 방법으로, Deadlock이 발생 할 수 있는지 시스템을 감시하고, 발생 가능성이 없을 때만 지원을 할당한다.
Deadlock detection and recovery: Deadlock을 탐지하고 복구하는 방법으로, Deadlock을 탐지할 경우, 프로세스를 정리하여 복구한다.
가상 메모리
가상 메모리는 메모리가 실제 메모리보다 많아 보이게 하는 기술이다.
일반적으로 보조기억장치의 일부분을 메모리처럼 사용하는 방법으로, 실제 프로그램이 메모리에 적재될 때, 전체가 아닌 필요한 부분만 적재되어도 된다는 점에서 기인한 방법이다.
가상 메모리를 사용하기 위해서, 메모리 주소를 가상 주소와 물리 주소 두가지로 나누어 사용하게 된다.
가상 주소: 프로세스가 사용하는 메모리 주소
물리 주소: 실제 메모리 상의 주소
프로그램에서 가상 주소를 통해 메모리에 접근하려고 하면, 이를 물리 주소로 변환하는 과정이 필요하다.
이 과정에는 MMU(Memory Management Unit)이 사용된다.
MMU
MMU는 가상 메모리 시스템에서 가상 주소를 물리 주소로 변환하는 역할을 하는 하드웨어 장치이다.
주소 변환 외에도 메모리 보호, 메모리 속성 관리 등의 작업을 수행한다.
Paging
Paging은 메모리를 고정된 크기의 Page로 분할하고, 연속되지 않은 위치에 데이터를 저장하는 기법이다.
이를 통해서 단편화 문제를 해결한다.
단편화
단편화를 간단하게 정리하자면, 메모리가 낭비되는 것이다.
내부 단편화는 할당된 메모리 공간이 실제 필요한 메모리 공간보다 클 때 공간이 낭비되는 현상을 말한다.
외부 단편화는 메모리 할당과 해제가 반복되면서, 프로세스에 할당할 수 없는 아주 작은 메모리 공간이 남게 되어 낭비되는 것을 말한다.
Page Fault
Page Fault는 가상 메모리 시스템에서, 접근하려는 Page가 실제 메모리 상에 존재하지 않을때 발생하는 예외이다.