ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java Multithreading] 경쟁상태RaceCondition와 데이터경쟁DataRace
    Java/Multi Threading 기초 2022. 11. 25. 12:05

    Java Multithreading

    멀티스레딩의 개념과 Java에서의 활용법을 공부하고 정리하는 시리즈입니다.


    앞선 글에서 경쟁 상태Race Condition와 데이터 경쟁Data Race이 계속 언급되어 왔는데요. 오늘은 두 개념의 차이에 대해 알아보겠습니다. 앞으로 한글 표기와 영문 표기는 편의에 따라 혼용하겠습니다.

    경쟁 상태 Race Condition

    경쟁상태의 핵심은 여러 스레드가 공유 자원에 비원자적 연산을 하는 것입니다. 실행 순서에 따라 연산의 결과값이 변할 수 있는 상태를 경쟁 상태라고 하는 것입니다.
    race condition을 극복하기 위해서는 앞서 살펴봤듯 race condition이 일어나는 critical section에 synchronized 블록을 지정하여 해결할 수가 있습니다.

    데이터 경쟁 Data Race

    저는 모르는 내용에 대해서 정의부터 들으면 알쏭달쏭 하더라구요. 먼저 알쏭달쏭하게 해드린 다음에😅 빠르게 예시를 들어 설명해 보겠습니다.

    data race는 컴파일러나 CPU가 실행문을 최적화 하는 과정에서 '우리가 코드를 보면서 예상할 수 있는 불변성'이 깨지는 것을 말합니다.

    class DataRaceClass {
        private int sharedValue1 = 1;
        private int sharedValue2 = 1;
    
        public void incrementValue() {
            sharedValue1++;
            sharedValue2++;
        }
    
        public void checkDataRace() {
            if (sharedValue1 < sharedValue2) {
                throw new DataRaceException("세상에 이런 일이?");
            }
        }
    }

    위는 data race를 설명하기 위한 예시 클래스입니다. 공유자원을 다루는 부분을 살펴보겠습니다.

        private int sharedValue1 = 1;
        private int sharedValue2 = 1;
    
        public void incrementValue() {
            sharedValue1++;
            sharedValue2++;
        }

    공유자원1과 2가 있습니다. 공유자원을 다루는 메서드incrementValue()는 공유자원1의 값을 먼저 증가시킨 다음 공유자원2의 값을 증가시킵니다.
    그렇게 되면 '우리가 코드를 보면서 예상할 수 있기로는', 이 메서드가 동작하는 어느 시점에든 공유자원1이 공유자원2보다 항상 1만큼 크거나 같다고 관찰될 것입니다. 다시 말해, 공유자원 2는 1보다 항상 작을 것이라는 점이 우리가 이 코드에 기대하는 '불변성'입니다.
    그럼 아래의 코드도 마저 살펴보겠습니다.

        public void checkDataRace() {
            if (sharedValue1 < sharedValue2) {
                throw new DataRaceException("세상에 이런 일이?");
            }
        }

    이 메서드에서는 우리가 기대하는 불변성이 깨지게 되는 경우, 즉 Data race가 일어나게 되는 경우 Exception을 던지게 했습니다.

    incrementValue()를 실행하면서 다른 스레드로 checkDataRace()를 통해 현 상태를 체크한다면 어떤 일이 일어날까요? Exception이 일어나지 않을까요?
    현재까지의 분위기로 미루어 봤을때 당연하게도😅 DataRaceException이 일어납니다. 경쟁 상태가 아닌데도 말이죠! 어떻게 이런 현상이 일어나는 걸까요?

    컴파일러나 CPU는 우리가 작성한 코드를 그대로 수행하지 않고 최적화를 거칩니다(무엇을 최적화 하는지는 언젠가 이야기 할 기회가 있으면 좋겠네요). 로직이 변경되지 않는 선에서 실행문은 변경되는 것이지요.
    예를 들어 int a = 1; int b = a + 2;와 같은 코드를 작성했다면, b를 초기화 하는 코드를 a 초기화 코드보다 먼저 실행하진 않을 겁니다. b의 값을 초기화 하는 코드는 a의 값에 종속되기 때문에, 이 코드를 뒤바꾼다면 로직이 아예 변경되는 것이기 때문입니다.
    하지만 우리가 예시로 작성한 경우는 이와 달랐습니다. '각각의 공유자원'에, '각각의 연산'을 행했습니다. 사실 sharedValue1++; sharedValue2++;라고 적든, sharedValue2++; sharedValue1++;라고 적든, CPU와 컴파일러의 입장에서는 아무 차이가 없는 동일한 코드나 다름 없습니다. 즉 멀티스레딩 환경에서 Data Race가 일어날 수 있는 코드인 것이죠.

    그렇다면 어떻게 Data race를 방지할 수 있을까요?

    • 첫번째 방법은 synchronized 키워드를 사용하는 것입니다. 우리가 앞서 많은 공유자원 문제를 해결했듯이, 똑같은 원리로 해결될 수 있습니다. 하지만 위의 예시처럼 Race Condition도 아닌데 synchronized를 통해서 성능 상의 페널티까지 감수하며 해결하는 방법이 되기도 합니다.
    • 두번째 방법은 접근되는 공유자원을 volatile로 선언하는 것입니다. volatile은 해당 공유자원에 접근하는 코드를 기점으로 전후의 코드가 뒤섞이지 않도록 보장하여 모든 종류의 Data race를 방지하게 됩니다.

    결론

    그렇다고 항상 volatile이 더 좋은 방법이라는 것은 아닙니다. race condition과 data race 상태를 파악해서 상황에 맞는 솔루션을 택해야 합니다.
    하지만 지금 시점에서 한 가지 확실히 알 수 있는 것은, 공유자원의 값을 쓰는 작업을 하는 경우에는 synchronized 또는 volatile 중 하나를 반드시 적용하는 것이 바람직하다는 것입니다.

    다음 포스팅에서 뵙겠습니다. 🥳

    댓글

Designed by Tistory.