ㄷㅣㅆㅣ's Amusement

[Android/Java] 변수를 Volatile로 선언하면? 본문

Programming/Android

[Android/Java] 변수를 Volatile로 선언하면?

ㄷㅣㅆㅣ 2016. 12. 6. 15:54

[Android/Java] 변수를 Volatile로 선언하면?

Volatile

멀티 쓰레드에서 volatile의 사용
(멀티 쓰레드 또는 병렬처리 부분은 다른 포스트를 참조하세요.)


들어가기 이전에...

volatile 미국식 [|vɑ:lətl], 영국식 [|vɒlətaɪl]

사실 멀티 쓰레드 프로그래밍을 하더라도 volatile을 잘 쓸 기회가 없었기에 만 8년동안 개발에 있으면서도 정확한 발음을 알지 못했다.
"발러틀"이라고 읽도록 하자 ㅡ,.ㅡ;; 

 
  • non-volatile로 선언했을 때 문제가 되는 경우
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class testVolatile {
        private int mCount = 0;
     
        public void runCountThread() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (;;) {
                        Log.d("Test Volatile""count : " + mCount++;);
                    }
                }
            }).run();
        }
    }
    cs
    •  위의 코드로 예를 들어보자. runCountThread()메소드를 두번 호출하면 어떻게 될까?
      1. 각각의 thread는 mCount의 값을 출력하고 1씩 증가시킨다.
      2. 그런데 여기서 나오는 로그들이 1,2,3,4,5,6,7... 순으로 의도대로 출력될까?? 
    • 위의 코드를 작성하고 실행해본다면 count의 값이 순차적이지 않고 1,1,2,2,3,3,4,4,5,5... 와 같이 출력되는 것을 볼 수 있다.


  • non-volatile로 선언시 문제가 발생하는 이유
    • 일반적으로 변수에 값을 할당하면 위와같이 먼저 CPU의 캐쉬에 저장하게 된다.
    • 이후 Memory에 쓰게 되는데, 이 메모리에 쓰는 작업은 OS/Platform에 따라 다르고, 이 시점은 프로그래머가 감지할 수 없다.
    • 따라서, 각기 다른 CPU(또는 Core, 또는 물리적 thread)에서 동작하는 thread는 최초에 mCount값(0)을 Memory로 부터 가져와서 Cache에 저장되어있는 값을 대상으로만 +1을 해준다.
    • +1을 해주더라도 Cache에서 언제 메모리로 쓰여지는지에 대한 컨트롤은 프로그래머가 할 수 없다.
    • 로그를 찍어보면 각자 자기가 가지고있는 캐쉬영역의 값을 찍기 때문에 같은 값이 겹쳐서 보여진다. (물론 상황에 따라서 OS/Platform이 연산하기 전에 Memory에 썼고, 이후 다른 쓰레드에서 메모리에서 가져오는 동작을 했다면 겹치지 않는 영역이 생길 수도 있다. 아무튼 이경우도 문제가 없는 것은 아니니..)
    • 이런 경우를 방지하기 위해 사용하는 것이 Volatile이고, 이런 문제를 "가시성(Visibility)"문제라고 한다.


  • Volatile을 사용하여 순서대로 카운트하기
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class testVolatile {
        private volatile int mCount = 0;
     
        public void runCountThread() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (;;) {
                        Log.d("Test Volatile""count : " + mCount++;);
                    }
                }
            }).start();
        }
    }
    cs
    • 이렇게 volatile로 선언하면 이후 모든 cache에 있는 값을 변경하는 작업에 대해서 memory로 쓰는 것을 보장한다.
    • 당연히 값을 읽어올 때마다 memory에서 읽어오는 것을 보장.


  • Volatile로 선언한 멤버변수의 또다른 속성
    • 최적화 방지
      1
      2
      3
      4
      5
      6
      7
      8
      public void testVolatile() {
          int a = 0;
          for (int i=0; i<100; i++) {
              a++;
          }
       
          Log.d("Test Volatile""a : " + a);
      }
      cs
      • 위의 코드를 보자. 프로그래머의 의도는 a를 100까지 순차적으로 증가시킨 후 a의 값을 출력하는 것이다.
      • 그러나 위의 코드는 JVM에 의해서 (C/C++에서는 컴파일러에 의해서) 다음과 같이 바뀐다

        1
        2
        3
        4
        5
        6
        7
        public void testVolatile() {
            int a = 0;
            
            = 100;
         
            Log.d("Test Volatile""a : " + a);
        }
        cs
      • 어떠한 상황에서도 a가 100이 아닌 값을 가질 수 없어졌다는 것.
      • volatile로 선언하면 이러한 최적화에 의한 의도변경도 일어나지 않는다.

    • synchronized vs volatile
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public class testVolatile {
          private volatile int mCount = 0;
          private int a = 0;
       
          public void runCountThread() {
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      for (;;) {
                          a += ++mCount;
                          Log.d("Test Volatile""count : " + a);
                      }
                  }
              }).start();
          }
      }
      cs
      • 이번에도 위의 예제에 있는 runCountThread()를 두번 이상 호출했다고 가정하자.
      • 어떤 결과가 나오게 될지 유추하는 것이 좀 복잡하다.
      • 10행의 코드는 연산이 단 한번으로 끝나는 것이 아니라 다음의 4가지 동작이 일어나기 때문이다. (만약 32비트cpu에서 long으로 선언한 변수였다면 더 많은 동작이 일어난다)
        1. mCount를 1 증가
        2. mCount를 int로 캐스팅
        3. a 와 mCount를 더한다
        4. 3번의 값을 a에 할당한다.
      • 따라서 multi thread환경에서 a의 값을 출력한다면, 어떤 쓰레드는 mCount를 더한값을, 어떤 쓰레드는 a의 값을 프로그래머가 의도하지도 않았고, 컨트롤 할 수도 없게 나타내게 된다.
      • 이 경우에는 volatile 대신 synchronized를 쓴다면 10번 줄의 모든 연산까지도 원자성을 보장하게된다.





0 Comments
댓글쓰기 폼