it-swarm-ko.tech

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

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

285
Doron Yaacoby

나는 Eric Lippert (원본에서 강조)보다 이것에 대답하는 더 좋은 사람이 없다고 생각합니다.

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

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

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

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

262
Ohad Schneider

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)로 수행하지 않고 메모리를 단일 명령으로 직접 수정합니다 (추가 mem , reg). 코드가 생성 된 방식으로 단일 CPU 코어의 컨텍스트 내에서 루프 카운터의 값이 항상 최신 상태로 유지됩니다. 데이터를 조작하지 않으면 손상이나 데이터 손실이 발생할 수 있습니다 (따라서 inc 중에 값이 변경되어 상점에서 손실되므로로드/inc/store를 사용하지 않음) 인터럽트는 현재 명령어가 완료된 후에 만 ​​서비스 될 수 있으므로 정렬되지 않은 메모리로도 데이터가 손상 될 수 없습니다.

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

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

휘발성 키워드는 IO이 (가) 지속적으로 변경되지만 메모리 매핑 된 IO 장치와 같이 일정한 주소를 갖는 UART 작업에 사용되도록 고안되었습니다. 컴파일러는 주소에서 읽은 첫 번째 값을 계속 재사용해서는 안됩니다.

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

55
Skizz

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

  1. 스레드 1은 변수가 널인지 묻습니다. //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

38
AndrewTek

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

Volatile 키워드는 컴파일러에게 "이 값을 메모리에 저장하고 싶다"고 말하는 것으로 생각할 수 있습니다. 이를 통해 두 번째 스레드가 최신 값을 검색합니다.

22
Benoit

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

21
Dr. Bob

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

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

13
Joseph Daigle

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

1
Paul Easter

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

 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입니다. 따라서 예상되는 논리가 깨질 수 있습니다.

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

private static volatile int _flag = 0;

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

0
Aliaksei Maniuk