.NET에서 이중 체크 잠금에서 휘발성 수정 자 필요
여러 텍스트는 .NET에서 이중 검사 잠금을 구현할 때 잠그고있는 필드에 휘발성 수정자를 적용해야한다고 말합니다. 하지만 정확히 왜? 다음 예를 고려하십시오.
public sealed class Singleton
{
private static volatile Singleton instance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
"잠금 (syncRoot)"이 필요한 메모리 일관성을 달성하지 못하는 이유는 무엇입니까? "잠금"문 이후 읽기와 쓰기가 모두 휘발성이므로 필요한 일관성이 달성된다는 것이 사실이 아닙니까?
휘발성은 불필요합니다. 음, 일종의 **
volatile
변수에 대한 읽기와 쓰기 사이에 메모리 장벽 *을 만드는 데 사용됩니다.
lock
를 사용하면 블록에 lock
대한 액세스를 하나의 스레드로 제한하는 것 외에도 내부 블록 주위에 메모리 장벽이 생성됩니다 .
메모리 장벽은 각 스레드가 변수의 최신 값 (일부 레지스터에 캐시 된 로컬 값이 아님)을 읽고 컴파일러가 명령문을 다시 정렬하지 않도록합니다. 사용은 volatile
이미 잠금을 가지고 있기 때문에 ** 필요하지 않습니다.
Joseph Albahari 는 내가 할 수있는 것보다 더 잘 설명합니다.
그리고 C # 에서 싱글 톤 구현에 대한 Jon Skeet의 가이드 를 확인하십시오 .
update :
* volatile
변수의 읽기는 VolatileRead
s가되고 쓰기는 s가되며 VolatileWrite
, 이는 CLR의 x86 및 x64에서 MemoryBarrier
. 다른 시스템에서는 더 세밀 할 수 있습니다.
** 내 대답은 x86 및 x64 프로세서에서 CLR을 사용하는 경우에만 정확합니다. Mono (및 기타 구현), Itanium64 및 향후 하드웨어와 같은 다른 메모리 모델에서도 마찬가지 일 수 있습니다. 이것이 Jon이 이중 체크 잠금에 대한 "gotchas"기사에서 언급 한 내용입니다.
약한 메모리 모델 상황에서 코드가 제대로 작동하려면 {변수를로 표시 volatile
,로 읽 Thread.VolatileRead
거나에 대한 호출 삽입 Thread.MemoryBarrier
} 중 하나를 수행 해야 할 수 있습니다.
내가 이해 한 바에 따르면 CLR (IA64에서도)에서는 쓰기가 다시 정렬되지 않습니다 (쓰기에는 항상 릴리스 의미가 있음). 그러나 IA64에서는 휘발성으로 표시되지 않는 한 읽기가 쓰기 전에 오도록 순서를 변경할 수 있습니다. 안타깝게도 IA64 하드웨어에 액세스 할 수 없기 때문에 이에 대해 제가 말하는 것은 추측 일 것입니다.
: 나는 또한 도움이 기사를 발견했습니다
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
밴스 모리슨의 기사 (이에 대한 모든 링크가 두 번 잠금 확인에 대해 이야기)
크리스 brumme의 기사 (이에 대한 모든 링크 )
Joe Duffy : 이중 체크 잠금의 깨진 변형
luis abreu의 멀티 스레딩 시리즈는 개념에 대한 멋진 개요도 제공합니다 .
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http : // msmvps. com / blogs / luisabreu / archive / 2009 / 07 / 03 / multithreading-introducing-memory-fences.aspx
volatile
필드 없이 구현하는 방법이 있습니다 . 설명하겠습니다 ...
잠금 외부에서 완전히 초기화되지 않은 인스턴스를 얻을 수 있도록 위험한 것은 잠금 내부의 메모리 액세스 재정렬이라고 생각합니다. 이것을 피하기 위해 다음을 수행합니다.
public sealed class Singleton
{
private static Singleton instance;
private static object syncRoot = new Object();
private Singleton() {}
public static Singleton Instance
{
get
{
// very fast test, without implicit memory barriers or locks
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
{
var temp = new Singleton();
// ensures that the instance is well initialized,
// and only then, it assigns the static variable.
System.Threading.Thread.MemoryBarrier();
instance = temp;
}
}
}
return instance;
}
}
}
코드 이해
Singleton 클래스의 생성자 내부에 초기화 코드가 있다고 상상해보십시오. 새 객체의 주소로 필드를 설정 한 후 이러한 명령어가 재정렬되면 불완전한 인스턴스가있는 것입니다. 클래스에 다음 코드가 있다고 상상해보십시오.
private int _value;
public int Value { get { return this._value; } }
private Singleton()
{
this._value = 1;
}
이제 new 연산자를 사용하여 생성자를 호출한다고 상상해보십시오.
instance = new Singleton();
다음 작업으로 확장 할 수 있습니다.
ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;
이 지침을 다음과 같이 재정렬하면 어떻게됩니까?
ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;
차이가 있습니까? 단일 스레드를 생각하면 아니오 . 여러 스레드를 생각하면 예 ... 스레드가 중단 된 직후 set instance to ptr
:
ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
-- thread interruped here, this can happen inside a lock --
set ptr._value to 1; -- Singleton.instance is not completelly initialized
이것이 메모리 액세스 재정렬을 허용하지 않음으로써 메모리 장벽이 피하는 것입니다.
ptr = allocate memory for Singleton;
set temp to ptr; // temp is a local variable (that is important)
set ptr._value to 1;
-- memory barrier... cannot reorder writes after this point, or reads before it --
-- Singleton.instance is still null --
set Singleton.instance to temp;
즐거운 코딩 되세요!
나는 사람이 실제로 응답 한 생각하지 않는다 질문을 나는 그것을 시도 줄거야, 그래서.
The volatile and the first if (instance == null)
are not "necessary". The lock will make this code thread-safe.
So the question is: why would you add the first if (instance == null)
?
The reason is presumably to avoid executing the locked section of code unnecessarily. While you are executing the code inside the lock, any other thread that tries to also execute that code is blocked, which will slow your program down if you try to access the singleton frequently from many threads. Depending on the language/platform, there could also be overheads from the lock itself that you wish to avoid.
So the first null check is added as a really quick way to see if you need the lock. If you don't need to create the singleton, you can avoid the lock entirely.
But you can't check if the reference is null without locking it in some way, because due to processor caching, another thread could change it and you would read a "stale" value that would lead you to enter the lock unnecessarily. But you're trying to avoid a lock!
So you make the singleton volatile to ensure that you read the latest value, without needing to use a lock.
You still need the inner lock because volatile only protects you during a single access to the variable - you can't test-and-set it safely without using a lock.
Now, is this actually useful?
Well I would say "in most cases, no".
If Singleton.Instance could cause inefficiency due to the locks, then why are you calling it so frequently that this would be a significant problem? The whole point of a singleton is that there is only one, so your code can read and cache the singleton reference once.
The only case I can think of where this caching wouldn't be possible would be when you have a large number of threads (e.g. a server using a new thread to process every request could be creating millions of very short-running threads, each of which would have to call Singleton.Instance once).
So I suspect that double checked locking is a mechanism that has a real place in very specific performance-critical cases, and then everybody has clambered on the "this is the proper way to do it" bandwagon without actually thinking what it does and whether it will actually be necessary in the case they are using it for.
AFAIK (and - take this with caution, I'm not doing a lot of concurrent stuff) no. The lock just gives you synchronization between multiple contenders (threads).
volatile on the other hand tells your machine to reevaluate the value every time, so that you don't stumble upon a cached (and wrong) value.
See http://msdn.microsoft.com/en-us/library/ms998558.aspx and note the following quote:
Also, the variable is declared to be volatile to ensure that assignment to the instance variable completes before the instance variable can be accessed.
A description of volatile: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx
You should use volatile with the double check lock pattern.
Most people point to this article as proof you do not need volatile: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10
But they fail to read to the end: "A Final Word of Warning - I am only guessing at the x86 memory model from observed behavior on existing processors. Thus low-lock techniques are also fragile because hardware and compilers can get more aggressive over time. Here are some strategies to minimize the impact of this fragility on your code. First, whenever possible, avoid low-lock techniques. (...) Finally, assume the weakest memory model possible, using volatile declarations instead of relying on implicit guarantees."
If you need more convincing then read this article on the ECMA spec will be used for other platforms: msdn.microsoft.com/en-us/magazine/jj863136.aspx
If you need further convincing read this newer article that optimizations may be put in that prevent it from working without volatile: msdn.microsoft.com/en-us/magazine/jj883956.aspx
In summary it "might" work for you without volatile for the moment, but don't chance it write proper code and either use volatile or the volatileread/write methods. Articles that suggest to do otherwise are sometimes leaving out some of the possible risks of JIT/compiler optimizations that could impact your code, as well us future optimizations that may happen that could break your code. Also as mentioned assumptions in the last article previous assumptions of working without volatile already may not hold on ARM.
The lock
is sufficient. The MS language spec (3.0) itself mentions this exact scenario in §8.12, without any mention of volatile
:
A better approach is to synchronize access to static data by locking a private static object. For example:
class Cache { private static object synchronizationObject = new object(); public static void Add(object x) { lock (Cache.synchronizationObject) { ... } } public static void Remove(object x) { lock (Cache.synchronizationObject) { ... } } }
I think that I've found what I was looking for. Details are in this article - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10.
To sum up - in .NET volatile modifier is indeed not needed in this situation. However in weaker memory models writes made in constructor of lazily initiated object may be delayed after write to the field, so other threads might read corrupt non-null instance in the first if statement.
This a pretty good post about using volatile with double checked locking:
http://tech.puredanger.com/2007/06/15/double-checked-locking/
In Java, if the aim is to protect a variable you don't need to lock if it's marked as volatile
'program story' 카테고리의 다른 글
알 수없는 크기의 std :: array를 함수에 전달 (0) | 2020.10.08 |
---|---|
Windows 아래 Git : MSYS 또는 Cygwin? (0) | 2020.10.08 |
직접 실행 창에서 데이터 테이블 또는 데이터보기의 내용을 쉽게 볼 수있는 방법 (0) | 2020.10.07 |
UIColor에서 rgb 추출 (0) | 2020.10.07 |
파일이 bash에 있는지 테스트하는 While 루프 (0) | 2020.10.07 |