2013-04-12 6 views
5

GetNextNumber()이 동시에 두 스레드에 의해 호출되는 경우 아래 코드에서 두 스레드에 동일한 수를 반환 할 수 있습니까?정적 변수를 사용하는 동시성

class Counter 
{ 
private static int s_Number = 0; 
public static int GetNextNumber() 
{ 
    s_Number++; 
    return s_Number; 
} 
} 

이유를 설명 할 수 있습니까?

EDIT : 코드가 두 스레드에 동일한 번호를 반환하는 것이 가능한 경우 다음과 같습니까? 두 스레드가 s_Number이 2 일 때 GetNextNumber()을 호출한다고 가정 해 봅니다. 동일한 값이 반환 된 경우이 값은 4로만 가능합니다. 3 일 수 없습니다. 맞습니까?

+8

네, 그것은 가능성의 –

+0

가능하지만이 가능하기 때문에이 상황을 처리해야합니다. 나는 lock 키워드를 사용할 것이다. – DGibbs

+5

을 보장 할 수 없습니다하지만 그것은, 가능 –

답변

10

ㅁㅁ 같은 간단한 카운터를 다루는 KN, 그것을 사용하는 것이 가장 좋습니다 Interlocked.Increment :이 (한 수가 오버 플로우를하지 않는 한) 고유 한 값을 반환 각 스레드을 보장하고, 어떤 단위가 손실되지 않는다는

private static int s_Number = 0; 

public static int GetNextNumber() 
{ 
    return Interlocked.Increment(ref s_Number); 
} 

. 읽기 s_Numbers_Number

  • 저장소에 새 값을 추가 1 s_Number
  • 의 기존 값을 읽어

    1. : 원래의 코드가 다음 단계로 세분화 될 수 있기 때문에

    2. 읽기 값을 반환하십시오.
    발생할 수 1,363,210

    시나리오는 다음과 같습니다

    1. 두 스레드가 같은 값에 끝나는 1 일까지 같은 기존의 값을 읽어 두 스레드를 의미 나머지, 증가하기 전에 1 단계를 실행합니다. 증분 손실
    2. 스레드는 1 - 3 단계를 충돌없이 실행할 수 있지만 두 스레드가 변수를 업데이트 한 후 4 단계를 실행하여 동일한 값을 검색합니다.

      private readonly object _SomeLock = new object(); 
      
      ... 
      
      lock (_SomeLock) 
      { 
          // only 1 thread allowed in here at any one time 
          // manipulate the data structures here 
      } 
      

      그러나 같은 간단한 조각을 위해 : 더 많은 데이터가 lock 문이 일반적으로 더 나은 방법입니다, 원자 액세스 할 필요가 코드의 더 큰 조각의 번호

    을 건너 뜀 필드의 값을 원자 적으로 늘리고 새 값을 검색하는 것만으로도 코드의 정확성이 향상됩니다. Interlocked.Increment이 더 좋고 빠르며 코드가 적습니다.

    Interlocked 클래스의 다른 메소드도 있습니다.이 메소드는 처리하는 시나리오에서 매우 편리합니다.

    에 대한 자세한 설명은 증가분이입니다.

    두 개의 스레드를 실행하기 전에의 0에서 그 s_Number 시작을 가정 해 봅시다 :

    Thread 1       Thread 2 
    Read s_Number = 0 
                Read s_Number = 0 
    Add 1 to s_Number, getting 1 
                Add 1 to s_Number, getting 1 (same as thread 1) 
    Store into s_Number (now 1) 
                Store into s_Number (now 1) 
    Read s_Number = 1 
                Read s_Number = 1 
    Return read value (1) 
                Return read value (1) 
    

    당신이 s_Number의 최종 값이 2를 봤는데한다 위에서 볼 수 있듯이, 그리고 스레드 중 하나가 반환해야했습니다 1, 나머지 2입니다. 대신에 최종 값은 1이었고 두 스레드 모두 1을 반환했습니다. 여기에서 증분을 잃었습니다.

    s_Number의 최종 결과가 올 2 만 1 반환했습니다해야 스레드 중 하나가 될 것입니다 번호 여기

    Thread 1       Thread 2 
    Read s_Number = 0 
    Add 1 to s_Number, getting 1 
    Store into s_Number (now 1) 
                Read s_Number = 1 
                Add 1 to s_Number, getting 2 
                Store into s_Number (now 2) 
    Read s_Number = 2 
                Read s_Number = 2 
    Return read value (2) 
                Return read value (2) 
    

    을 건너 뛰는

    자세한 설명은, 대신에 그들은 둘 다 2

    을 반환

    원래 코드가 IL 수준에서 어떻게 보이는지 보겠습니다. 재생하려는 경우 나, 최적화 (소형/O + 오른쪽 하단)을 가능하게

    // public static int GetNumber() 
    // { 
    GetNumber: 
    //  s_Number++; 
    IL_0000: ldsfld  UserQuery.s_Number // step 1: Read s_Number 
    IL_0005: ldc.i4.1       // step 2: Add 1 to it 
    IL_0006: add        //   (part of step 2) 
    IL_0007: stsfld  UserQuery.s_Number // step 3: Store into s_Number 
    // return s_Number; 
    IL_000C: ldsfld  UserQuery.s_Number // step 4: Read s_Number 
    IL_0011: ret        // step 5: Return the read value 
    // } 
    

    주, 나는 위의 IL 코드를 얻을 수 LINQPad를 사용 주석과 IL 지침에 원래의 코드를 추가 할 것 이 IL로 변환하는 방법을 볼 수있는 코드로, LINQPad을 다운로드하고이 프로그램을 공급 :

    void Main() { } // Necessary for LINQPad/Compiler to be happy 
    
    private static int s_Number = 0; 
    public static int GetNumber() 
    { 
        s_Number++; 
        return s_Number; 
    } 
    
  • +0

    이 솔루션에 대한 감사와 +1을 알지 못했습니다! –

    +0

    @Lasse +1 시나리오. 잃어버린 시나리오를 설명해 주시겠습니까? 내가 복용하는 라인은 : s_Number가 스택의 값이기 때문에 두 번 증가하면 증가분을 잃을 수 없다. – bytefire

    +0

    괜찮아. 알았어. 모든 스레드가 자체 스택을 가져오고 값 유형이 복사된다는 사실을 잊어 버렸습니다. – bytefire

    8

    예, 여기에 시나리오 :

    s_number ++

    s_number = 1

    Thread B해야합니까 = 0

    Thread A s_number s_number ++

    s_number = 2

    것이다

    class Counter 
    { 
        private static int s_Number = 0; 
        private static object _locker = new object(); 
        public static int GetNextNumber() 
        { 
         //Critical section 
         return Interlocked.Increment(ref s_Number); 
        } 
    } 
    

    잠금 장치 :

    Thread A는이 같은 잠금 메커니즘을 구현해야 return s_number

    두 스레드가 2


    따라서 반환 할 return s_number

    Thread B 할 여러 스레드가 y를 입력하지 못하도록합니다. 동시에 우리의 비판적 섹션. 단순 증분보다 많은 연산을 수행하는 경우 Lock 블록을 대신 사용하십시오.

    편집 : 더 낮은 수준의 동작을 설명하는 Lasse V. Karlsen이 더 심층적 인 대답을했습니다.

    +1

    +1,이 상황에서 필자는 아마도''Interlocked.Increment' (http://msdn.microsoft.com/en-us/library/system.threading.interlocked.increment.aspx)를 사용할 것입니다. 전체'lock'. – LukeH

    +0

    @ 루크 말린, 고마워. 그러나 문제를 해결하는 방법을 알고 있습니다. 내가 관심을 갖고있는 것이 그 이유입니다. – bytefire

    +1

    @bytefire가 –

    0

    return Interlocked.Increment (ref s_Number);

    이렇게 할 것입니다. 잠금을 사용하는 것보다 훨씬 간단합니다. 잠금 블록은 일반적으로 코드 블록에 주로 사용되어야합니다.

    1

    두 개의 스레드 우리가 IL 아래

    class Counter 
    { 
        private static int s_Number = 0; 
        public static int GetNextNumber() 
        { 
         s_Number++; 
         return s_Number; 
        } 
    } 
    

    여기에 클래스 메소드에 대해 생성 IL code 보는 경우 동시에 GetNextNumber 방법에 액세스하려고 할 때 같은 수를 얻을 수 있습니다 이유를 쉽게 알 수있다 코드가 생성되고, 알다시피, s_number++은 실제로 두 스레드가 동시에 액세스하여 동일한 초기 값을 얻을 수있는 세 개의 개별 명령어로 구성됩니다.

    Counter.GetNextNumber: 
    IL_0000: ldsfld  UserQuery+Counter.s_Number 
    IL_0005: ldc.i4.1  
    IL_0006: add   
    IL_0007: stsfld  UserQuery+Counter.s_Number 
    IL_000C: ldsfld  UserQuery+Counter.s_Number 
    IL_0011: ret   
    

    이는이 시점에서, 값 1로드되지만 두 스레드

    thread A가 들어가고 s_Number (IL_0000)의 값을 취득

    동일한 값에 이르게 시나리오 프로세서는 thread A을 일시 중단하고 thread B을 시작합니다. 물론 s_number에 정의 된 메모리 위치에 저장된 값은 여전히 ​​0이고 스레드 B는 스레드 A와 동일한 값으로 시작합니다. 스레드는 다시 1이됩니다. 스레드 A가 다시 시작되면 레지스터는 일시 중지 시간에 복원되고, 그래서 0에 1을 가산 스레드 B. 동일한 결과를 다시 얻을

    이 클래스는 동시성

    class CounterLocked 
    { 
    private static object o; 
    private static int s_Number = 0; 
    public static int GetNextNumber() 
    { 
        lock(o) 
        { 
        s_Number++; 
        return s_Number; 
        } 
    } 
    } 
    
    CounterLocked.GetNextNumber: 
    IL_0000: ldc.i4.0  
    IL_0001: stloc.0  // <>s__LockTaken0 
    IL_0002: ldsfld  UserQuery+CounterLocked.o 
    IL_0007: dup   
    IL_0008: stloc.2  // CS$2$0001 
    IL_0009: ldloca.s 00 // <>s__LockTaken0 
    IL_000B: call  System.Threading.Monitor.Enter 
    IL_0010: ldsfld  UserQuery+CounterLocked.s_Number 
    IL_0015: ldc.i4.1  
    IL_0016: add   
    IL_0017: stsfld  UserQuery+CounterLocked.s_Number 
    IL_001C: ldsfld  UserQuery+CounterLocked.s_Number 
    IL_0021: stloc.1  // CS$1$0000 
    IL_0022: leave.s  IL_002E 
    IL_0024: ldloc.0  // <>s__LockTaken0 
    IL_0025: brfalse.s IL_002D 
    IL_0027: ldloc.2  // CS$2$0001 
    IL_0028: call  System.Threading.Monitor.Exit 
    IL_002D: endfinally 
    IL_002E: ldloc.1  // CS$1$0000 
    IL_002F: ret   
    

    InterlockIncrement 위해 생성 된 코드는 매우 간단을 차단하는 로크 키워드를 사용

    public static int GetNextNumber() 
    { 
        return Interlocked.Increment(ref s_Number); 
    } 
    
    CounterLocked.GetNextNumber: 
    IL_0000: ldsflda  UserQuery+CounterLocked.s_Number 
    IL_0005: call  System.Threading.Interlocked.Increment 
    IL_000A: ret 
    
    +0

    그것은 매우 도움이됩니다. 제 질문의 새 EDIT 섹션을보실 수 있습니까? – bytefire

    관련 문제