C #에서 volatile 키워드를 사용하는 경우

공용 언어 런타임에서 JIT (just-in-time) 컴파일러가 사용하는 최적화 기술은 .Net 프로그램이 다중 스레드 시나리오에서 데이터의 비 휘발성 읽기를 수행하려고 할 때 예측할 수없는 결과를 초래할 수 있습니다. 이 기사에서는 휘발성 및 비 휘발성 메모리 액세스의 차이점, C #에서 volatile 키워드의 역할 및 volatile 키워드를 사용하는 방법을 살펴 봅니다.

개념을 설명하기 위해 C #으로 몇 가지 코드 예제를 제공합니다. volatile 키워드가 작동하는 방식을 이해하려면 먼저 JIT 컴파일러 최적화 전략이 .Net에서 작동하는 방식을 이해해야합니다.

JIT 컴파일러 최적화 이해

JIT 컴파일러는 최적화 전략의 일부로 프로그램의 의미와 최종 출력을 변경하지 않는 방식으로 읽기 및 쓰기 순서를 변경한다는 점에 유의해야합니다. 이것은 아래 주어진 코드 스 니펫에 설명되어 있습니다.

x = 0;

x = 1;

위의 코드 조각은 프로그램의 원래 의미를 유지하면서 다음과 같이 변경할 수 있습니다.

x = 1;

JIT 컴파일러는 "상수 전파"라는 개념을 적용하여 다음 코드를 최적화 할 수도 있습니다.

x = 1;

y = x;

위의 코드 스 니펫은 프로그램의 원래 의미를 유지하면서 다시 다음과 같이 변경할 수 있습니다.

x = 1;

y = 1;

휘발성 대 비 휘발성 메모리 액세스

현대 시스템의 메모리 모델은 매우 복잡합니다. 프로세서 레지스터, 다양한 수준의 캐시 및 여러 프로세서가 공유하는 주 메모리가 있습니다. 프로그램이 실행될 때 프로세서는 데이터를 캐시 한 다음 실행중인 스레드에서 요청하면 캐시에서이 데이터에 액세스 할 수 있습니다. 이 데이터의 업데이트 및 읽기는 캐시 된 데이터 버전에 대해 실행될 수 있으며 주 메모리는 나중에 업데이트됩니다. 이 메모리 사용 모델은 다중 스레드 응용 프로그램에 영향을 미칩니다. 

한 스레드가 캐시의 데이터와 상호 작용하고 두 번째 스레드가 동일한 데이터를 동시에 읽으려고 할 때 두 번째 스레드는 주 메모리에서 오래된 버전의 데이터를 읽을 수 있습니다. 이는 비 휘발성 개체의 값이 업데이트 될 때 주 메모리가 아닌 실행중인 스레드의 캐시에서 변경되기 때문입니다. 그러나 휘발성 객체의 값이 업데이트되면 실행중인 스레드의 캐시에서 변경된 내용이있을뿐만 아니라이 캐시가 주 메모리로 플러시됩니다. 그리고 휘발성 객체의 값을 읽으면 스레드는 캐시를 새로 고치고 업데이트 된 값을 읽습니다.

C #에서 volatile 키워드 사용

C #의 volatile 키워드는 변수 값이 운영 체제, 하드웨어 또는 동시 실행 스레드에 의해 변경 될 수 있으므로 JIT 컴파일러에 절대로 캐시되지 않아야 함을 알리는 데 사용됩니다. 따라서 컴파일러는 데이터 충돌, 즉 변수의 다른 값에 액세스하는 다른 스레드로 이어질 수있는 변수에 대한 최적화를 사용하지 않습니다.

객체 또는 변수를 휘발성으로 표시하면 휘발성 읽기 및 쓰기의 후보가됩니다. C #에서 모든 메모리 쓰기는 데이터를 휘발성 또는 비 휘발성 개체에 쓰는지 여부에 관계없이 휘발성이라는 점에 유의해야합니다. 그러나 데이터를 읽을 때 모호성이 발생합니다. 비 휘발성 데이터를 읽을 때 실행중인 스레드는 항상 최신 값을 얻거나 얻지 못할 수 있습니다. 객체가 휘발성이면 스레드는 항상 최신 값을 가져옵니다.

volatile키워드 앞에 변수를 휘발성으로 선언 할 수 있습니다 . 다음 코드 스 니펫은이를 보여줍니다.

수업 프로그램

    {

        공개 휘발성 int i;

        static void Main (string [] args)

        {

            // 여기에 코드 작성

        }

    }

volatile참조, 포인터 및 열거 형 유형과 함께 키워드를 사용할 수 있습니다 . byte, short, int, char, float 및 bool 유형과 함께 volatile 한정자를 사용할 수도 있습니다. 지역 변수는 휘발성으로 선언 할 수 없습니다. 참조 유형 객체를 휘발성으로 지정하면 포인터 (객체가 실제로 저장된 메모리의 위치를 ​​가리키는 32 비트 정수) 만 인스턴스 값이 아니라 휘발성입니다. 또한 double 변수는 x86 시스템의 단어 크기보다 크기가 64 비트이므로 휘발성 일 수 없습니다. double 변수를 휘발성으로 만들어야하는 경우 클래스 내부에 래핑해야합니다. 아래 코드 스 니펫에 표시된대로 래퍼 클래스를 만들어 쉽게 수행 할 수 있습니다.

공개 클래스 VolatileDoubleDemo

{

    개인 휘발성 WrappedVolatileDouble volatileData;

}

공용 클래스 WrappedVolatileDouble

{

    public double Data {get; 세트; }

그러나 위 코드 예제의 제한 사항에 유의하십시오. volatileData참조 포인터 의 최신 값이 있더라도 Data속성 의 최신 값이 보장되지는 않습니다 . 이에 대한 해결 방법은 WrappedVolatileDouble유형을 변경 불가능 하게 만드는 것 입니다.

volatile 키워드는 특정 상황에서 스레드 안전에 도움이 될 수 있지만 모든 스레드 동시성 문제에 대한 해결책은 아닙니다. 변수 나 객체를 휘발성으로 표시한다고해서 lock 키워드를 사용할 필요가 없다는 의미는 아닙니다. volatile 키워드는 lock 키워드를 대체하지 않습니다. 동일한 데이터에 액세스하려는 여러 스레드가있을 때 데이터 충돌을 방지하는 데 도움이됩니다.