IT TIP

C #의 변수에 액세스하는 것은 원자 적 작업입니까?

itqueen 2020. 11. 24. 20:46
반응형

C #의 변수에 액세스하는 것은 원자 적 작업입니까?


여러 스레드가 변수에 액세스 할 수있는 경우 프로세서가 중간에 다른 스레드로 전환 할 수 있으므로 해당 변수에 대한 모든 읽기 및 쓰기는 "잠금"문과 같은 동기화 코드로 보호되어야한다고 믿게되었습니다. 쓰기.

그러나 Reflector를 사용하여 System.Web.Security.Membership을 살펴보고 다음과 같은 코드를 찾았습니다.

public static class Membership
{
    private static bool s_Initialized = false;
    private static object s_lock = new object();
    private static MembershipProvider s_Provider;

    public static MembershipProvider Provider
    {
        get
        {
            Initialize();
            return s_Provider;
        }
    }

    private static void Initialize()
    {
        if (s_Initialized)
            return;

        lock(s_lock)
        {
            if (s_Initialized)
                return;

            // Perform initialization...
            s_Initialized = true;
        }
    }
}

잠금 외부에서 s_Initialized 필드를 읽는 이유는 무엇입니까? 다른 스레드가 동시에 쓰기를 시도 할 수 없습니까? 변수의 읽기 및 쓰기는 원자 적입니까?


확실한 대답은 사양으로 이동하십시오. :)

파티션 I, CLI 사양의 섹션 12.6.6은 다음과 같이 명시되어 있습니다. "적합한 CLI는 위치에 대한 모든 쓰기 액세스가 동일한 크기 일 때 기본 단어 크기보다 크지 않은 적절하게 정렬 된 메모리 위치에 대한 읽기 및 쓰기 액세스가 원자적임을 보장해야합니다. . "

따라서 s_Initialized가 절대 불안정하지 않으며 32 비트보다 작은 primitve 유형에 대한 읽기 및 쓰기가 원자적임을 확인합니다.

특히, doublelong( Int64UInt64)는 32 비트 플랫폼에서 원 자성이 보장 되지 않습니다 . Interlocked클래스 의 메서드를 사용하여 이를 보호 할 수 있습니다 .

또한 읽기 및 쓰기는 원자 적이지만 원시 유형을 읽고, 조작하고, 다시 작성해야하기 때문에 더하기, 빼기, 증가 및 감소가있는 경쟁 조건이 있습니다. 연동 클래스를 사용하면 CompareExchangeIncrement메서드를 사용하여이를 보호 할 수 있습니다 .

인터 로킹은 프로세서가 읽기 및 쓰기 순서를 변경하지 못하도록 메모리 장벽을 만듭니다. 잠금은이 예에서 필요한 유일한 장벽을 만듭니다.


이것은 C #에서 스레드로부터 안전 하지 않은 이중 검사 잠금 패턴의 (나쁜) 형태입니다 !

이 코드에는 한 가지 큰 문제가 있습니다.

s_Initialized는 휘발성이 아닙니다. 즉, s_Initialized가 true로 설정된 후 초기화 코드의 쓰기가 이동할 수 있고 s_Initialized가 true 인 경우에도 다른 스레드가 초기화되지 않은 코드를 볼 수 있습니다. 이는 모든 쓰기가 휘발성 쓰기이기 때문에 Microsoft의 프레임 워크 구현에는 적용되지 않습니다.

그러나 Microsoft의 구현에서도 초기화되지 않은 데이터의 읽기 순서를 다시 지정할 수 있습니다 (예 : cpu에 의해 미리 가져 오기). . 읽기 순서가 바뀝니다).

예를 들면 :

Thread 1 reads s_Provider (which is null)  
Thread 2 initializes the data  
Thread 2 sets s\_Initialized to true  
Thread 1 reads s\_Initialized (which is true now)  
Thread 1 uses the previously read Provider and gets a NullReferenceException

s_Initialized를 읽기 전에 s_Provider의 읽기를 이동하는 것은 어디에도 휘발성 읽기가 없기 때문에 완벽하게 합법적입니다.

s_Initialized가 휘발성이면 s_Initialized를 읽기 전에 s_Provider의 읽기를 이동할 수 없으며 s_Initialized가 true로 설정되고 이제 모든 것이 정상으로 설정되면 Provider의 초기화도 이동할 수 없습니다.

Joe Duffy는 또한이 문제에 대한 기사를 썼습니다 . 이중 검사 잠금에 대한 깨진 변형


잠시만 요-제목에있는 질문은 Rory가 묻는 실제 질문이 아닙니다.

제목 형 질문에는 "아니오"라는 간단한 대답이 있습니다.하지만 실제 질문을 보면 전혀 도움이되지 않습니다. 누구도 간단한 대답을하지 않았다고 생각합니다.

Rory가 묻는 진짜 질문은 훨씬 나중에 제시되며 그가 제공하는 예와 더 관련이 있습니다.

잠금 외부에서 s_Initialized 필드를 읽는 이유는 무엇입니까?

이에 대한 답은 간단하지만 변수 접근의 원자 성과는 전혀 관련이 없습니다.

잠금이 비싸기 때문에 s_Initialized 필드는 잠금 외부에서 읽습니다 .

s_Initialized 필드는 본질적으로 "한 번 쓰기"이므로 절대 오 탐지를 반환하지 않습니다.

자물쇠 밖에서 읽는 것이 경제적입니다.

이것은 이익을 얻을 가능성이 높은 저비용 활동입니다 .

이것이 표시되지 않는 한 잠금 사용 비용을 지불하지 않기 위해 잠금 외부에서 읽히는 이유입니다.

자물쇠가 저렴하다면 코드가 더 간단 할 것이며 첫 번째 검사를 생략하십시오.

(편집 : rory의 좋은 반응은 다음과 같습니다. 예, 부울 읽기는 매우 원자 적입니다. 누군가가 비 원자 부울 읽기로 프로세서를 구축했다면 DailyWTF에 포함됩니다.)


정답은 "예, 대부분"입니다.

  1. CLI 사양을 참조하는 John의 답변은 32 비트 프로세서에서 32 비트보다 크지 않은 변수에 대한 액세스가 원자적임을 나타냅니다.
  2. C # spec, 섹션 5.5, Atomicity of variable reference 에서 추가 확인 :

    bool, char, byte, sbyte, short, ushort, uint, int, float 및 reference 유형과 같은 데이터 유형의 읽기 및 쓰기는 원자 적입니다. 또한 이전 목록의 기본 유형이있는 열거 형 유형의 읽기 및 쓰기도 원자 적입니다. long, ulong, double 및 decimal을 포함한 다른 유형의 읽기 및 쓰기와 사용자 정의 유형은 원 자성이 보장되지 않습니다.

  3. 내 예제의 코드는 ASP.NET 팀이 작성한 것처럼 Membership 클래스에서 다른 표현으로 표현되었으므로 s_Initialized 필드에 액세스하는 방식이 정확하다고 가정하는 것이 항상 안전했습니다. 이제 우리는 이유를 압니다.

편집 : Thomas Danecker가 지적했듯이 필드 액세스가 원자 적이지만 s_Initialized는 읽기 및 쓰기 순서를 변경하는 프로세서에 의해 잠금이 해제되지 않도록 휘발성 으로 표시되어야합니다 .


초기화 기능에 결함이 있습니다. 다음과 같이 보일 것입니다.

private static void Initialize()
{
    if(s_initialized)
        return;

    lock(s_lock)
    {
        if(s_Initialized)
            return;
        s_Initialized = true;
    }
}

잠금 내부의 두 번째 검사가 없으면 초기화 코드가 두 번 실행될 수 있습니다. 따라서 첫 번째 검사는 불필요하게 잠금을 취하는 것을 방지하기위한 성능이고, 두 번째 검사는 스레드가 초기화 코드를 실행하고 있지만 아직 s_Initialized플래그를 설정하지 않았기 때문에 두 번째 스레드가 첫 번째 검사를 통과하고 자물쇠에서 기다리고 있습니다.


변수의 읽기 및 쓰기는 원 자성이 아닙니다. 원자 적 읽기 / 쓰기를 에뮬레이트하려면 동기화 API를 사용해야합니다.

이것에 대한 멋진 참조와 동시성과 관련된 더 많은 문제를 보려면 Joe Duffy의 최신 스펙터클 사본을 확인하십시오 . 리퍼입니다!


"C #에서 변수에 액세스하는 것은 원자 적 작업입니까?"

Nope. And it's not a C# thing, nor is it even a .net thing, it's a processor thing.

OJ is spot on that Joe Duffy is the guy to go to for this kind of info. ANd "interlocked" is a great search term to use if you're wanting to know more.

"Torn reads" can occur on any value whose fields add up to more than the size of a pointer.


@Leon
I see your point - the way I've asked, and then commented on, the question allows it to be taken in a couple of different ways.

To be clear, I wanted to know if it was safe to have concurrent threads read and write to a boolean field without any explicit synchronization code, i.e., is accessing a boolean (or other primitive-typed) variable atomic.

I then used the Membership code to give a concrete example, but that introduced a bunch of distractions, like the double-check locking, the fact that s_Initialized is only ever set once, and that I commented out the initialization code itself.

My bad.


You could also decorate s_Initialized with the volatile keyword and forego the use of lock entirely.

That is not correct. You will still encounter the problem of a second thread passing the check before the first thread has had a chance to to set the flag which will result in multiple executions of the initialisation code.


I think you're asking if s_Initialized could be in an unstable state when read outside the lock. The short answer is no. A simple assignment/read will boil down to a single assembly instruction which is atomic on every processor I can think of.

I'm not sure what the case is for assignment to 64 bit variables, it depends on the processor, I would assume that it is not atomic but it probably is on modern 32 bit processors and certainly on all 64 bit processors. Assignment of complex value types will not be atomic.


I thought they were - I'm not sure of the point of the lock in your example unless you're also doing something to s_Provider at the same time - then the lock would ensure that these calls happened together.

Does that //Perform initialization comment cover creating s_Provider? For instance

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Otherwise that static property-get's just going to return null anyway.


Perhaps Interlocked gives a clue. And otherwise this one i pretty good.

I would have guessed that their not atomic.


To make your code always work on weakly ordered architectures, you must put a MemoryBarrier before you write s_Initialized.

s_Provider = new MemershipProvider;

// MUST PUT BARRIER HERE to make sure the memory writes from the assignment
// and the constructor have been wriitten to memory
// BEFORE the write to s_Initialized!
Thread.MemoryBarrier();

// Now that we've guaranteed that the writes above
// will be globally first, set the flag
s_Initialized = true;

The memory writes that happen in the MembershipProvider constructor and the write to s_Provider are not guaranteed to happen before you write to s_Initialized on a weakly ordered processor.

A lot of thought in this thread is about whether something is atomic or not. That is not the issue. The issue is the order that your thread's writes are visible to other threads. On weakly ordered architectures, writes to memory do not occur in order and THAT is the real issue, not whether a variable fits within the data bus.

EDIT: Actually, I'm mixing platforms in my statements. In C# the CLR spec requires that writes are globally visible, in-order (by using expensive store instructions for every store if necessary). Therefore, you don't need to actually have that memory barrier there. However, if it were C or C++ where no such guarantee of global visibility order exists, and your target platform may have weakly ordered memory, and it is multithreaded, then you would need to ensure that the constructors writes are globally visible before you update s_Initialized, which is tested outside the lock.


An If (itisso) { check on a boolean is atomic, but even if it was not there is no need to lock the first check.

If any thread has completed the Initialization then it will be true. It does not matter if several threads are checking at once. They will all get the same answer, and, there will be no conflict.

The second check inside the lock is necessary because another thread may have grabbed the lock first and completed the initialization process already.


What you're asking is whether accessing a field in a method multiple times atomic -- to which the answer is no.

In the example above, the initialise routine is faulty as it may result in multiple initialization. You would need to check the s_Initialized flag inside the lock as well as outside, to prevent a race condition in which multiple threads read the s_Initialized flag before any of them actually does the initialisation code. E.g.,

private static void Initialize()
{
    if (s_Initialized)
        return;

    lock(s_lock)
    {
        if (s_Initialized)
            return;
        s_Provider = new MembershipProvider ( ... )
        s_Initialized = true;
    }
}

Ack, nevermind... as pointed out, this is indeed incorrect. It doesn't prevent a second thread from entering the "initialize" code section. Bah.

You could also decorate s_Initialized with the volatile keyword and forego the use of lock entirely.

참고URL : https://stackoverflow.com/questions/9666/is-accessing-a-variable-in-c-sharp-an-atomic-operation

반응형