개발 블로그
article thumbnail
Published 2023. 4. 12. 21:22
[TIL] DAY 10 항해99

오늘은 동시성 문제에 대해서 학습하면서 알게 된 새로운 개념들에 대해 좀 정리를 해보려고 한다.

 

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();
    }
}

thread1 에서 while 문을 탈출하고 오랜만이야!! 를 출력했습니다.

'항해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
profile

개발 블로그

@하얀.손

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!