멀티스레드와 동시성

Ch05. 메모리 가시성 - volatile, 메모리 가시성

webmaster 2024. 8. 8. 09:19
728x90

VolatileV1

실행 코드
실행 결과

  • 프로그램은 아주 간단하다. runFlag를 사용해서 스레드의 작업을 종료한다.
  • 예상 시나리오
    • "work"스레드는 MyTask를 실행한다. 여기에는 runFlag를 체크하는 무한 루프가 있다.
    • runFlag 값이 false 가 되면 무한 루프를 탈출하며 작업을 종료한다.
    • 이후에 "main "스레드가 runFlag의 값을 false로 변경한다.
    • runFlag의 값이 false 되었으므로 "work" 스레드는 무한 루프를 탈출하며, 작업을 종료한다.
  • 실제 실행 결과를 보면 task 종료 출력되지 않는다! 그리고 자바 프로그램도 멈추지 않고 계속 실행된다. 정확히는 work 스레드가 while문에서 빠져나오지 못하고 있는 것이다.

메모리 그림

  • "main" 스레드, "work" 스레드 모두 MyTask 인스턴스( x001 )에 있는 runFlag를 사용한다.
  • 값을 false로 변경하면 work 스레드의 작업을 종료할 있다.

참고: 실행 환경에 따라 실제 실행 결과는 달라질 수 있다. 어떤 분들은 특정 시간이 지나고 task 종료가 출력될 수 도 있고,, 어떤 분들은 기대하는 실행 결과와 똑같이 작동할 수도 있다. 가급적 예제 코드와 똑같이 작성해야 비슷한 결과를 얻을 가능성이 높아진다.**

메모리 가시성 문제

멀티스레드는 이미 여러 스레드가 작동해서 그래도 이해하기 어려운데, 거기에 한술 더하는 문제가 있으니, 바로 메모리 가시성(memory visibility) 문제이다. 이게이게 어떤 문제이고, 이런 문제가 발생하는지, 그리고 어떻게 해결하는지 그림으로 차근차근 알아보자.

일반적인 생각(잘못된 Flow)

일반적인 상황에서의 메모리 가시성 이슈1
일반적인 상황에서의 메모리 가시성 이슈2
일반적인 상황에서의 메모리 가시성 이슈3

  • main 스레드와 work 스레드는 각각의 CPU 코어에 할당되어서 실행된다. 물론 CPU 코어가 1개라면 빠르게 번갈아 가면서 실행될 수 있다.
  • 점선 위쪽은 스레드의 실행 흐름을 나타내고, 점선 아래쪽은 하드웨어를 나타낸다.
  • 자바 프로그램을 실행하고 main 스레드와 work 스레드는 모두 메인 메모리의 runFlag의 값을 읽는다. 프로그램의 시작 시점에는 runFlag를 변경하지 않기 때문에 모든 스레드에서 true의 값을 읽는다.
    • 참고로 runFlag의 초기값은 true이다.
  • work 스레드의 경우 while(runFlag [true]) 가 만족하기 때문에 while문을 계속 반복해서 수행한다.

실제 상황에서의 메모리 가시성 이슈 : CPU는 처리 성능을 개선하기 위해 중간에 캐시 메모리라는 것을 사용한다.

실제 상황에서의 메모리 가시성 이슈 - 1

  • 메인 메모리는 CPU 입장에서 보면 거리도 멀고, 속도도 상대적으로 느리다. 대신에 상대적으로 가격이 저렴해서 큰 용량을 쉽게 구성할 수 있다.
  • CPU 연산은 매우 빠르기 때문에 CPU 연산의 빠른 성능을 따라가려면, CPU 가까이에 매우 빠른 메모리가 필요한데, 이것이 바로 캐시 메모리이다. 캐시 메모리는 CPU와 가까이 붙어있고, 속도도 매우 빠른 메모리이다. 하지만 상대적으로 가격이 비싸기 때문에 큰 용량을 구성하기는 어렵다.
  • 현대의 CPU 대부분은 코어 단위로 캐시 메모리를 각각 보유하고 있다.
  • 참고로 여러 코어가 공유하는 캐시 메모리도 있다.

실제 상황에서의 메모리 가시성 이슈 - 2

  • 스레드가 runFlag의 값을 사용하면 CPU 값을 효율적으로 처리하기 위해 먼저 runFlag를 캐시 모리에 불러온다.
  • 그리고 이후에는 캐시 메모리에 있는 runFlag를 사용하게 된다.

실제 상황에서의 메모리 가시성 이슈 - 3

  • 점선 위쪽은 스레드의 실행 흐름을 나타내고, 점선 아래쪽은 하드웨어를 나타낸다.
  • 자바 프로그램을 실행하고 main 스레드와 work 스레드는 모두 runFlag의 값을 읽는다.
  • CPU는 이 값을 효율적으로 처리하기 위해 먼저 캐시 메모리에 불러온다.
  • "main" 스레드와 "work" 스레드가 사용하는 runFlag  각각의 캐시 메모리에 보관된다.
  • 프로그램의 시작 시점에는 runFlag를 변경하지 않기 때문에 모든 스레드에서 true의 값을 읽는다. 
    • 참고로 runFlag의 초기값은 true이다.
  • "work" 스레드의 경우 while(runFlag [true])  만족하기 때문에 while문을 계속 반복해서 수행한다.

실행 흐름별 실제 상황에서의 메모리 가시성 이슈 - 1
실행 흐름별 실제 상황에서의 메모리 가시성 이슈 - 2

  • 캐시 메모리의 runFlag 값만 변한다는 것이다! 메인 메모리에 이 값이 즉시 반영되지 않는다.
    • "main" 스레드가 runFlag의 값을 변경해도 CPU 코어 1이 사용하는 캐시 메모리의 runFlag 값만 false로 변경된다.
    • "work" 스레드가 사용하는 CPU 코어 2의 캐시 메모리의 runFlag 값은 여전히 true이다.
    • "work" 스레드의 경우 while(runFlag [true]) 가 만족하기 때문에 while문을 계속 반복해서 수행한다.
  • 캐시 메모리에 있는 runFlag의 값이 언제 메인 메모리에 반영될까?
    • 이 부분에 대한 정답은 "알 수 없다"이다. CPU 설계 방식과 종류의 따라 다르다. 극단적으로 보면 평생 반영되지 않을 수도 있다
    • 메인 메모리에 반영을 한다고 해도, 문제는 여기서 끝이 아니다.
    • 메인 메모리에 반영된 runFlag 값을 "work" 스레드가 사용하는 캐시 메모리에 다시 불러와야 한다.
  • 메인 메모리에 변경된 runFlag 값이 언제 CPU 코어 2의 캐시 메모리에 반영될까?
    • 부분에 대한 정답도 " 없다"이다. CPU 설계 방식과 종류의 따라 다르다. 극단적으로 보면 평생 반영되지 않을 수도 있다!
    • 언젠가 CPU 코어 2의 캐시 메모리에 runFlag 값을 불러오게 되면 "work" 스레드가 확인하는 runFlag의 false 되므로 while문을 탈출하고, "task 종료"를 출력한다.

캐시 메모리를 메인 메모리에 반영하거나, 메인 메모리의 변경 내역을 캐시 메모리에 다시 불러오는 것은 언제 발생할까?
이 부분은 CPU 설계 방식과 실행 환경에 따라 다를 수 있다. 즉시 반영될 수도 있고, 몇 밀리초 후에 될 수도 있고, 몇 초 후에 될 수도 있고, 평생 반영되지 않을 수도 있다.

주로 콘텍스트 스위칭이 될 때, 캐시 메모리도 함께 갱신되는데, 이 부분도 환경에 따라 달라질 수 있다.
예를 들어 Thread.sleep()이나 콘솔에 내용을 출력할 때 스레드가 잠시 쉬는데, 이럴 때 콘텍스트 스위칭이 되면서 주로 갱신된다. 하지만 이것이 갱신을 보장하는 것은 아니다.

 

메모리 가시성(memory visibility)
이처럼 멀티스레드 환경에서 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 문제를 메모리 가시성 (memory visibility)이라 한다. 이름 그대로 메모리에 변경한 값이 보이는가, 보이지 않는가의 문제이다.

VolatileV2

실행 코드
실행 결과

  • 기존 코드에서 boolean runFlag 앞에 volatile이라는 키워만 하나 추가해 보자.
    • volatile boolean runFlag = true;
  • 이렇게 하면 runFlag에 대해서는 캐시 메모리를 사용하지 않고, 값을 읽거나 쓸 때 항상 메인 메모리에 직접 접근한다.
  • 실행 결과를 보면 runFlag를 변경 하자마자 "task 종료"가 출력되는 것을 확인할 있다. 그리고 모든 스레드가 정상 종료되기 때문에 자바 프로그램도 종료된다.

실행 결과 분석 - 시간의 흐름

  • 여러 스레드에서 같은 값을 읽고 써야 한다면 volatile 키워드를 사용하면 된다. 단 캐시 메모리를 사용할 때 보다 성능이 느려지는 단점이 있기 때문에 꼭! 필요한 곳에만 사용하는 것이 좋다.

 

728x90