오늘은 동시성 문제에 대해서 학습하면서 알게 된 새로운 개념들에 대해 좀 정리를 해보려고 한다.
1. 원자성
2. 가시성 & CPU Cache
원자성
int i = 1;
i = i + 2;
이렇게 코드를 실행하면 2번째 줄의 코드는 한 줄이지만 실제로는 총 3번의 연산이 이루어지는 것이다.
1. i 에 담긴 값을 읽어오고(Read)
2. 여기에 2를 더하고(Modify)
3. 다시 i 에 덮어써준다.(Write)
그래서 각각의 연산 사이에 시간텀이 발생하게 되고 만약 그 시간텀에 다른 스레드에서 i 라는 자원에 접근하면 예상치 못한 결과를 야기할 수 있는 것이다.
원자성이란 공유 자원에 대한 작업의 단위가 더 이상 쪼갤 수 없는 하나의 연산인 것처럼 동작하는것을 의미한다.
당장 i = i + 2 라는 작업은 총 3 번의 연산으로 이루어지므로 원자성을 보이고 있지 않은데
동기화를 통해서 i 라는 자원을 임의의 스레드가 사용하고 있으면 다른 스레드는 사용하지 못 하도록 할 수 있다.
이 경우 i = i + 2 라는 작업이 다른 스레드가 끼어들 시간텀이 없는 것처럼 동작해 원자성을 갖게 되는 것이다.
가시성 & CPU Cache
가시성
가시성은 말 그대로 볼 수 있냐?다.
스레드 간 공유 자원에 대해서 어떤 스레드가 공유 자원을 수정했을 경우 다른 스레드에서도 이를 확인할 수 있느냐?를 따지는 것이다.
그냥 생각했을 때는 스레드1 이 메모리에 있는 자원을 수정하면 스레드2 가 그 다음에 여기 접근했을 때는 당연히 확인할 수 있는 거 아닌가? 이런 개념이 왜 있는 거지? 라고 생각할 수 있다.
예제를 통해서 가시성이라는 개념이 실제로 유의미한가부터 확인해보자.
public class Test {
private static boolean Arrived = false;
public static void main(String[] args) throws InterruptedException {
Runnable task1 = () -> {
while(!Arrived){}
System.out.println("오랜만이야!!");
};
Runnable task2 = () -> {
Arrived = true;
};
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
Thread.sleep(5000);
thread2.start();
thread1.join();
}
}
오늘은 오랜 옛 친구를 만나는 날이다. 먼저 약속 장소에 도착해서 계속 친구가 왔는지 확인하다가 친구가 왔으면 while 문을 탈출하고 "오랜만이야!!"를 출력한다.
지금 코드에서는 약속 장소에 친구가 왔는지 확인하는 thread1 이 실행되고 5초 후에 thread2가 실행되어 친구가 도착한다.
그럼 예상하기로는 친구가 도착했으니 "오랜만이야"를 출력하고 thread1 이 종료되고 프로세스도 종료될 것이다.
하지만 이 코드를 실제로 실행시켜보면 무한루프에 빠져서 thread1 이 종료되지 않아 프로세스도 종료되지 않는다.
이는 thread2 가 Arrvied = true 로 만들었지만 thread1 이 이를 보지 못하고 있어서 발생하는 현상이다.
예제를 통해서 가시성이라는 개념이 유의미한 것은 확인했다. 그럼 이러한 현상의 발생 원인은 무엇일까?
바로 CPU Cache다.
CPU Cache
스레드는 CPU 위에서 실행이 되는데 이 때 CPU 와 메모리 간의 거리가 멀어서 CPU Cache 라는 것을 사용하게 된다.
위 예제와 함께 자세히 살펴보자.
우리의 메인 메모리에는 Arrived = false 로 초기화가 되어 있다.
이 때 thread1 이 실행되면서 메인 메모리의 Arrived = false를 가져와 자신의 CPU Cache에 담는다.
그리고 thread1 은 이 CPU Cache 에 담긴 Arrvied를 쳐다보면서 계속 while문을 돌고 있다.
메인 스레드가 5초 일시정지 상태에서 풀린 뒤 thread2가 실행된다.
thread2 역시 메인 메모리의 Arrived = false 를 자신의 CPU Cache 에 담고 Arrived = true 연산을 수행한다.
연산을 마치고 난 후에는 Arrived = true를 메인 메모리에게 전달해 메인 메모리의 Arrived가 true로 바뀔 수 있게 해준다.
여기까지 마쳤으면 thread2는 종료상태가 되고 메인 메모리의 Arrived 는 true 가 됐다.
이 때 우리 원하는 것은 thread1 이 메인 메모리의 Arrived 를 확인해서 while 문을 탈출하는 것인데 실제로는 CPU Cache 에 담긴 Arrived 를 확인하고 있기에 false 를 확인하고 while 문을 탈출하지 못하게 되는 것이다.
JAVA에서는 이를 해결하는 방법으로 volatile 을 제시하고 있다.
Arrived 변수에 volatile을 붙이면 이 변수에 대한 Read-Modify-Write 작업은 CPU Cache를 거치지 않고 메인 메모리에서 바로 이루어진다.
public class Test {
private volatile static boolean Arrived = false;
public static void main(String[] args) throws InterruptedException {
Runnable task1 = () -> {
while(!Arrived){}
System.out.println("오랜만이야!!");
};
Runnable task2 = () -> {
Arrived = true;
};
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
Thread.sleep(5000);
thread2.start();
thread1.join();
}
}
'항해99' 카테고리의 다른 글
[TIL] DAY12 (0) | 2023.04.15 |
---|---|
[TIL] DAY 11 (0) | 2023.04.13 |
[TIL] Day 9 (0) | 2023.04.12 |
[TIL] Day8 (0) | 2023.04.10 |
[WIL] 항해 1주차 (0) | 2023.04.09 |