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

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



답변

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

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

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

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

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


답변

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) 잠금 시스템이 필요합니다.


답변

.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


답변

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

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


답변

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


답변

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

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


답변

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

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