C #에서 volatile 키워드는 언제 사용해야합니까?


299

누구나 C #의 휘발성 키워드에 대한 좋은 설명을 제공 할 수 있습니까? 어떤 문제가 해결되고 어떤 문제가 해결되지 않습니까? 어떤 경우에 잠금 사용이 절약됩니까?


6
잠금 사용을 왜 절약하고 싶습니까? 일정하지 않은 잠금은 프로그램에 몇 나노초를 추가합니다. 실제로 몇 나노초를 감당할 수 없습니까?
Eric Lippert

답변:


273

에릭 리퍼 트 ( Eric Lippert) 보다 더 나은 사람이 있다고 생각하지 않습니다 (원본의 강조).

C #에서 "휘발성"은 "컴파일러와 지터가이 변수에 대해 코드 재정렬 또는 레지스터 캐싱 최적화를 수행하지 않아야 함"을 의미하지 않습니다. 또한 "다른 프로세서를 중지하고 메인 메모리를 캐시와 동기화하도록하는 경우에도 최신 값을 읽도록하기 위해 필요한 모든 작업을 수행하도록 프로세서에 알리십시오".

사실, 마지막 비트는 거짓말입니다. 휘발성 읽기 및 쓰기의 진정한 의미는 여기서 설명한 것보다 훨씬 더 복잡합니다. 사실 그들은 모든 프로세서가 수행중인 작업을 멈추고 있다는 사실을 보장하지 않는 메인 메모리에서 /에 업데이트 캐시. 오히려, 읽기 및 쓰기 전후의 메모리 액세스가 서로에 대해 정렬되는 방식에 대한 약한 보증을 제공합니다 . 새 스레드 작성, 잠금 입력 또는 Interlocked 메소드 중 하나를 사용하는 것과 같은 특정 조작은 순서 관찰에 대한 강력한 보증을 제공합니다. 자세한 내용을 보려면 C # 4.0 사양의 3.10 및 10.5.3 단원을 읽으십시오.

솔직히, 나는 당신이 휘발성이있는 분야를 만들지 말 것을 권합니다 . 휘발성 필드는 당신이 완전히 미친 짓을하고 있다는 표시입니다. 잠금을 설정하지 않고 두 개의 다른 스레드에서 동일한 값을 읽고 쓰려고합니다. 잠금은 잠금 내에서 읽거나 수정 한 메모리가 일관된 것으로 보이며 잠금은 한 번에 하나의 스레드 만 주어진 메모리 청크에 액세스하는 등을 보장합니다. 잠금이 너무 느린 상황의 수는 매우 적으며 정확한 메모리 모델을 이해하지 못하기 때문에 코드가 잘못 될 가능성은 매우 큽니다. Interlocked 연산의 가장 사소한 사용법을 제외하고는 낮은 잠금 코드를 작성하려고 시도하지 않습니다. 나는 "휘발성"의 사용법을 실제 전문가에게 맡긴다.

자세한 내용은 다음을 참조하십시오.


29
할 수 있으면 투표를하겠습니다. 거기에는 흥미로운 정보가 많이 있지만 실제로 그의 질문에 대답하지는 않습니다. 그는 잠금과 관련하여 휘발성 키워드의 사용에 대해 묻고 있습니다. 꽤 오래 (2.0 RT 이전), 필드 인스턴스에 생성자에 초기화 코드가있는 경우 정적 필드 스레드를 안전하게 만들려면 volatile 키워드가 필요했습니다 (AndrewTek의 답변 참조). 프로덕션 환경에는 여전히 1.1 RT 코드가 많이 있으며이를 유지 관리하는 개발자는 해당 키워드가 존재하는 이유와 제거하기에 안전한지 알아야합니다.
Paul Easter

3
@PaulEaster doulbe-checked 잠금 (일반적으로 싱글 톤 패턴)에 사용될 있다는 것이 반드시 그렇게 해야 한다는 것을 의미하지는 않습니다 . .NET 메모리 모델에 의존하는 것은 나쁜 습관 일 수 있습니다. 대신 ECMA 모델에 의존해야합니다. 예를 들어, 모델이 다른 모노로 포팅 할 수 있습니다. 또한 다른 하드웨어 아키텍처로 인해 변경 될 수 있음을 이해해야합니다. 자세한 내용은 stackoverflow.com/a/7230679/67824를 참조하십시오 . (모든 .NET 버전) 더 나은 싱글 대안은 다음을 참조하십시오 csharpindepth.com/articles/general/singleton.aspx
오핫 슈나이더

6
다시 말해, 질문에 대한 정답은 다음과 같습니다. 코드가 2.0 런타임 이상에서 실행되는 경우 volatile 키워드는 거의 필요하지 않으며 불필요하게 사용하면 좋은 것보다 더 해 롭습니다. 그러나 이전 버전의 런타임에서는 정적 필드에서 적절한 이중 검사 잠금이 필요합니다.
Paul Easter

3
이것은 잠금과 휘발성 변수가 다음과 같은 의미에서 상호 배타적이라는 것을 의미합니까? 일부 변수 주위에 잠금을 사용한 경우 더 이상 해당 변수를 휘발성으로 선언 할 필요가 없습니까?
giorgim

4
@Giorgi yes – 보장 된 메모리 장벽 volatile은 잠금으로 인해 거기에있을 것입니다
Ohad Schneider

54

volatile 키워드의 기능에 대해 좀 더 기술적으로 이해하려면 다음 프로그램을 고려하십시오 (DevStudio 2005를 사용하고 있습니다).

#include <iostream>
void main()
{
  int j = 0;
  for (int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  std::cout << j;
}

표준 최적화 (릴리스) 컴파일러 설정을 사용하여 컴파일러는 다음 어셈블러 (IA32)를 만듭니다.

void main()
{
00401000  push        ecx  
  int j = 0;
00401001  xor         ecx,ecx 
  for (int i = 0 ; i < 100 ; ++i)
00401003  xor         eax,eax 
00401005  mov         edx,1 
0040100A  lea         ebx,[ebx] 
  {
    j += i;
00401010  add         ecx,eax 
00401012  add         eax,edx 
00401014  cmp         eax,64h 
00401017  jl          main+10h (401010h) 
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
00401019  mov         dword ptr [esp],0 
00401020  mov         eax,dword ptr [esp] 
00401023  cmp         eax,64h 
00401026  jge         main+3Eh (40103Eh) 
00401028  jmp         main+30h (401030h) 
0040102A  lea         ebx,[ebx] 
  {
    j += i;
00401030  add         ecx,dword ptr [esp] 
00401033  add         dword ptr [esp],edx 
00401036  mov         eax,dword ptr [esp] 
00401039  cmp         eax,64h 
0040103C  jl          main+30h (401030h) 
  }
  std::cout << j;
0040103E  push        ecx  
0040103F  mov         ecx,dword ptr [__imp_std::cout (40203Ch)] 
00401045  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)] 
}
0040104B  xor         eax,eax 
0040104D  pop         ecx  
0040104E  ret              

출력을 보면 컴파일러는 ecx 레지스터를 사용하여 j 변수의 값을 저장하기로 결정했습니다. 비 휘발성 루프 (첫 번째)의 경우 컴파일러는 i를 eax 레지스터에 할당했습니다. 매우 간단합니다. lea ebx, [ebx] 명령어는 사실상 멀티 바이트 nop 명령어이므로 루프가 16 바이트로 정렬 된 메모리 주소로 점프합니다. 다른 하나는 inc eax 명령어 대신 루프 카운터를 증가시키기 위해 edx를 사용하는 것입니다. add reg, reg 명령어는 inc reg 명령어와 비교하여 일부 IA32 코어에서 대기 시간이 짧지 만 대기 시간이 더 길지는 않습니다.

휘발성 루프 카운터가있는 루프입니다. 카운터는 [esp]에 저장되며 volatile 키워드는 컴파일러에게 값을 항상 메모리에서 읽거나 쓰거나 레지스터에 할당해서는 안된다는 것을 알려줍니다. 컴파일러는 카운터 값을 업데이트 할 때로드 / 증가 / 저장을 세 가지 단계 (eax, inc eax, save eax)로 구분하지 않고 메모리를 단일 명령어 (add mem)로 직접 수정합니다. , reg). 코드가 생성 된 방식으로 단일 CPU 코어의 컨텍스트 내에서 루프 카운터의 값이 항상 최신 상태로 유지됩니다. 데이터를 조작하지 않으면 손상이나 데이터 손실이 발생할 수 있습니다 (따라서 inc 중에 값이 변경되어 상점에서 손실되기 때문에로드 / inc / store를 사용하지 않음). 현재 명령이 완료된 후에 만 ​​인터럽트를 서비스 할 수 있으므로

시스템에 두 번째 CPU를 도입하면 volatile 키워드는 다른 CPU가 동시에 업데이트하는 데이터를 보호하지 않습니다. 위의 예에서 데이터가 손상 될 수 있도록 정렬 해제해야합니다. volatile 키워드는 데이터를 원자 적으로 처리 할 수없는 경우 (예 : 루프 카운터가 long long (64 비트) 유형 인 경우 값을 업데이트하기 위해 두 개의 32 비트 작업이 필요합니다. 인터럽트가 발생할 수 있고 데이터를 변경할 수 있습니다.

따라서 volatile 키워드는 기본 레지스터의 크기보다 작거나 같은 정렬 된 데이터에만 적합하므로 작업이 항상 원자 적입니다.

volatile 키워드는 IO가 지속적으로 변경되지만 메모리 매핑 UART 장치와 같이 일정한 주소를 갖는 IO 작업에 사용되도록 고안되었으며 컴파일러는 주소에서 읽은 첫 번째 값을 계속 재사용하지 않아야합니다.

대용량 데이터를 처리하거나 여러 개의 CPU를 사용하는 경우 데이터 액세스를 올바르게 처리하려면 더 높은 수준 (OS) 잠금 시스템이 필요합니다.


이것은 C ++이지만 원칙은 C #에 적용됩니다.
Skizz

6
Eric Lippert는 C ++의 휘발성은 컴파일러가 일부 최적화를 수행하는 것을 막을뿐 아니라 C #의 휘발성은 추가로 다른 코어 / 프로세서 간의 일부 통신을 수행하여 최신 값을 읽도록합니다.
Peter Huber

44

.NET 1.1을 사용하는 경우 이중 검사 잠금을 수행 할 때 volatile 키워드가 필요합니다. 왜? .NET 2.0 이전에는 다음 시나리오에서 두 번째 스레드가 널이 아닌 완전히 구성되지 않은 오브젝트에 액세스 할 수 있습니다.

  1. 스레드 1은 변수가 null인지 묻습니다. //if(this.foo == null)
  2. 스레드 1은 변수가 널임을 판별하므로 잠금을 입력합니다. // 잠금 (this.bar)
  3. 스레드 1은 변수가 널인지 AGAIN에게 묻습니다. //if(this.foo == null)
  4. 스레드 1은 변수가 널인지 판별하므로 생성자를 호출하고 변수에 값을 지정합니다. //this.foo = 새로운 Foo ();

.NET 2.0 이전에는 생성자가 실행을 마치기 전에 this.foo에 새 Foo 인스턴스를 할당 할 수있었습니다. 이 경우 스레드 1이 Foo의 생성자를 호출하는 동안 두 번째 스레드가 들어 와서 다음을 경험할 수 있습니다.

  1. 스레드 2는 변수가 널인지 묻습니다. //if(this.foo == null)
  2. 스레드 2는 변수가 널이 아님을 판별하여 사용하려고합니다. //this.foo.MakeFoo ()

.NET 2.0 이전에는 this.foo를 휘발성으로 선언하여이 문제를 해결할 수있었습니다. .NET 2.0부터는 이중 확인 잠금을 수행하기 위해 더 이상 volatile 키워드를 사용할 필요가 없습니다.

Wikipedia는 실제로 Double Checked Locking에 대한 좋은 기사를 가지고 있으며 다음 주제에 대해 간략하게 설명합니다. http://en.wikipedia.org/wiki/Double-checked_locking


2
이것은 내가 레거시 코드에서 볼 수있는 것과 정확히 궁금합니다. 그래서 제가 더 깊이 연구를 시작했습니다. 감사!
피터 Porfy

1
Thread 2가 어떻게 가치를 할당 할 수 있는지 이해가되지 foo않습니까? 1 잠금 스레드가 없습니다 this.bar따라서 단지 1 시간에 givne 점에서 foo는 초기화 할 수 있습니다 스레드? 잠금 해제 후 다시 값을 확인합니다. 어쨌든 스레드 1의 새 값을 가져야합니다.
gilmishal

24

때때로 컴파일러는 필드를 최적화하고 레지스터를 사용하여 필드를 저장합니다. 스레드 1이 필드에 쓰기를 수행하고 다른 스레드가 필드에 액세스하면 업데이트가 메모리가 아닌 레지스터에 저장되었으므로 두 번째 스레드는 오래된 데이터를 얻습니다.

volatile 키워드를 컴파일러에게 "이 값을 메모리에 저장하길 원합니다"라고 생각할 수 있습니다. 이를 통해 두 번째 스레드가 최신 값을 검색합니다.


21

에서 MSDN : 휘발성 수정은 일반적으로 직렬화 액세스에 잠금 문을 사용하지 않고 여러 스레드에 의해 액세스되는 분야에 사용됩니다. 휘발성 수정자를 사용하면 한 스레드가 다른 스레드가 작성한 최신 값을 검색 할 수 있습니다.


13

CLR은 명령어를 최적화하기를 좋아하므로 코드에서 필드에 액세스 할 때 항상 필드의 현재 값에 액세스 할 수있는 것은 아닙니다 (스택 등일 수 있음). 필드를 표시하면 명령으로 필드 volatile의 현재 값에 액세스 할 수 있습니다. 프로그램의 동시 스레드 또는 운영 체제에서 실행중인 다른 코드로 값을 수정할 수있는 경우 (비 잠금 시나리오에서) 유용합니다.

분명히 일부 최적화가 손실되지만 코드를 더 단순하게 유지합니다.


3

Joydip Kanjilal 의이 기사가 매우 도움이되었습니다!

When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value

참고로 여기에 남겨 두겠습니다.


0

컴파일러는 때때로 코드에서 명령문 순서를 변경하여이를 최적화합니다. 일반적으로 단일 스레드 환경에서는 문제가되지 않지만 다중 스레드 환경에서는 문제가 될 수 있습니다. 다음 예를 참조하십시오.

 private static int _flag = 0;
 private static int _value = 0;

 var t1 = Task.Run(() =>
 {
     _value = 10; /* compiler could switch these lines */
     _flag = 5;
 });

 var t2 = Task.Run(() =>
 {
     if (_flag == 5)
     {
         Console.WriteLine("Value: {0}", _value);
     }
 });

t1 및 t2를 실행하면 결과가 없거나 "값 : 10"이 표시되지 않습니다. 컴파일러가 t1 함수 내부에서 라인을 전환 할 수 있습니다. t2가 실행되면 _flag의 값은 5이지만 _value의 값은 0입니다. 따라서 예상되는 논리가 깨질 수 있습니다.

이 문제를 해결 하기 위해 필드에 적용 할 수있는 휘발성 키워드를 사용할 수 있습니다. 이 명령문은 컴파일러 최적화를 비활성화하므로 코드에서 올바른 순서를 강제 할 수 있습니다.

private static volatile int _flag = 0;

특정 컴파일러 최적화를 비활성화하면 성능이 저하되므로 실제로 필요한 경우에만 휘발성 을 사용해야 합니다. 또한 모든 .NET 언어에서 지원되지는 않으므로 (Visual Basic은이를 지원하지 않음) 언어 상호 운용성을 방해합니다.


2
당신의 예는 정말 나쁩니다. 프로그래머는 t1의 코드가 먼저 작성된다는 사실에 근거하여 t2 태스크에서 _flag의 값을 기 대해서는 안됩니다. 먼저 작성! = 먼저 실행. 컴파일러가 t1에서 두 줄을 전환하는지 여부는 중요하지 않습니다. 컴파일러가 해당 명령문을 전환하지 않더라도 else 분기의 Console.WriteLne은 _flag의 volatile 키워드를 사용하더라도 여전히 실행될 수 있습니다.
Jakotheshadows

@ jakotheshadows, 당신 말이 맞아요, 나는 내 대답을 편집했습니다. 내 주요 아이디어는 우리가 t1과 t2를 동시에 실행할 때 예상되는 논리가 깨질 수 있음을 보여주는 것이 었습니다
Aliaksei Maniuk

0

따라서이 모든 것을 요약하면 질문에 대한 정답은 다음과 같습니다. 코드가 2.0 런타임 이상에서 실행되는 경우 volatile 키워드는 거의 필요하지 않으며 불필요하게 사용하면 좋은 것보다 더 해를 끼칩니다. IE 절대로 사용하지 마십시오. 그러나 이전 버전의 런타임에서는 정적 필드에서 적절한 이중 검사 잠금이 필요합니다. 정적 클래스 초기화 코드가있는 클래스가있는 정적 필드입니다.


-4

여러 스레드가 변수에 액세스 할 수 있습니다. 최신 업데이트는 변수에 있습니다

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.