답변:
에릭 리퍼 트 ( Eric Lippert) 보다 더 나은 사람이 있다고 생각하지 않습니다 (원본의 강조).
C #에서 "휘발성"은 "컴파일러와 지터가이 변수에 대해 코드 재정렬 또는 레지스터 캐싱 최적화를 수행하지 않아야 함"을 의미하지 않습니다. 또한 "다른 프로세서를 중지하고 메인 메모리를 캐시와 동기화하도록하는 경우에도 최신 값을 읽도록하기 위해 필요한 모든 작업을 수행하도록 프로세서에 알리십시오".
사실, 마지막 비트는 거짓말입니다. 휘발성 읽기 및 쓰기의 진정한 의미는 여기서 설명한 것보다 훨씬 더 복잡합니다. 사실 그들은 모든 프로세서가 수행중인 작업을 멈추고 있다는 사실을 보장하지 않는 메인 메모리에서 /에 업데이트 캐시. 오히려, 읽기 및 쓰기 전후의 메모리 액세스가 서로에 대해 정렬되는 방식에 대한 약한 보증을 제공합니다 . 새 스레드 작성, 잠금 입력 또는 Interlocked 메소드 중 하나를 사용하는 것과 같은 특정 조작은 순서 관찰에 대한 강력한 보증을 제공합니다. 자세한 내용을 보려면 C # 4.0 사양의 3.10 및 10.5.3 단원을 읽으십시오.
솔직히, 나는 당신이 휘발성이있는 분야를 만들지 말 것을 권합니다 . 휘발성 필드는 당신이 완전히 미친 짓을하고 있다는 표시입니다. 잠금을 설정하지 않고 두 개의 다른 스레드에서 동일한 값을 읽고 쓰려고합니다. 잠금은 잠금 내에서 읽거나 수정 한 메모리가 일관된 것으로 보이며 잠금은 한 번에 하나의 스레드 만 주어진 메모리 청크에 액세스하는 등을 보장합니다. 잠금이 너무 느린 상황의 수는 매우 적으며 정확한 메모리 모델을 이해하지 못하기 때문에 코드가 잘못 될 가능성은 매우 큽니다. Interlocked 연산의 가장 사소한 사용법을 제외하고는 낮은 잠금 코드를 작성하려고 시도하지 않습니다. 나는 "휘발성"의 사용법을 실제 전문가에게 맡긴다.
자세한 내용은 다음을 참조하십시오.
volatile
은 잠금으로 인해 거기에있을 것입니다
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 이전에는 다음 시나리오에서 두 번째 스레드가 널이 아닌 완전히 구성되지 않은 오브젝트에 액세스 할 수 있습니다.
.NET 2.0 이전에는 생성자가 실행을 마치기 전에 this.foo에 새 Foo 인스턴스를 할당 할 수있었습니다. 이 경우 스레드 1이 Foo의 생성자를 호출하는 동안 두 번째 스레드가 들어 와서 다음을 경험할 수 있습니다.
.NET 2.0 이전에는 this.foo를 휘발성으로 선언하여이 문제를 해결할 수있었습니다. .NET 2.0부터는 이중 확인 잠금을 수행하기 위해 더 이상 volatile 키워드를 사용할 필요가 없습니다.
Wikipedia는 실제로 Double Checked Locking에 대한 좋은 기사를 가지고 있으며 다음 주제에 대해 간략하게 설명합니다. http://en.wikipedia.org/wiki/Double-checked_locking
foo
않습니까? 1 잠금 스레드가 없습니다 this.bar
따라서 단지 1 시간에 givne 점에서 foo는 초기화 할 수 있습니다 스레드? 잠금 해제 후 다시 값을 확인합니다. 어쨌든 스레드 1의 새 값을 가져야합니다.
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
참고로 여기에 남겨 두겠습니다.
컴파일러는 때때로 코드에서 명령문 순서를 변경하여이를 최적화합니다. 일반적으로 단일 스레드 환경에서는 문제가되지 않지만 다중 스레드 환경에서는 문제가 될 수 있습니다. 다음 예를 참조하십시오.
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은이를 지원하지 않음) 언어 상호 운용성을 방해합니다.
여러 스레드가 변수에 액세스 할 수 있습니다. 최신 업데이트는 변수에 있습니다