2016-08-10 2 views
14

스레드 안전을 목표로하는 클래스를 구현할 때 내부 구조가 초기화되기 전에 초기화가 완료되었는지 확인하기 위해 생성자 끝에 메모리 장벽을 포함해야합니까? 아니면 다른 스레드에서 인스턴스를 사용할 수있게 만들기 전에 메모리 장벽을 삽입하는 것은 소비자의 책임입니까?스레드 안전 클래스는 생성자 끝에 메모리 장벽이 있어야합니까?

간체 질문 :

그 때문에 초기화 및 스레드 안전 클래스의 액세스와 메모리 장벽의 부족으로 잘못된 행동을 줄 수 아래의 코드에서 경주의 위험이 있습니까? 또는 스레드로부터 안전한 클래스 자체가이를 방지해야합니까?

ConcurrentQueue<int> queue = null; 

Parallel.Invoke(
    () => queue = new ConcurrentQueue<int>(), 
    () => queue?.Enqueue(5)); 

두 번째 대리자가 첫 번째 메시지보다 먼저 실행될 때와 같이 프로그램에서 아무 것도 대기열에 추가하지 않아도됩니다. (null 조건부 연산자 ?.은 여기에서 NullReferenceException에 대해 보호합니다.) 그러나 프로그램이 IndexOutOfRangeException, NullReferenceException, 대기열에 5을 여러 번 던지거나 무한 루프에 걸리거나 다른 이상한 행동을해서는 안됩니다 내부 구조에 대한 인종적 위험에 의한 것.

정교 질문 :

구체적으로는, 내가 큐에 대한 간단한 스레드 안전 래퍼를 구현되어 있다고 가정 해. (I는 .NET 이미 ConcurrentQueue<T> 제공하는 알고 있어요, 이것은 단지 예입니다.) 내가 쓸 수 :

public class ThreadSafeQueue<T> 
{ 
    private readonly Queue<T> _queue; 

    public ThreadSafeQueue() 
    { 
     _queue = new Queue<T>(); 

     // Thread.MemoryBarrier(); // Is this line required? 
    } 

    public void Enqueue(T item) 
    { 
     lock (_queue) 
     { 
      _queue.Enqueue(item); 
     } 
    } 

    public bool TryDequeue(out T item) 
    { 
     lock (_queue) 
     { 
      if (_queue.Count == 0) 
      { 
       item = default(T); 
       return false; 
      } 

      item = _queue.Dequeue(); 
      return true; 
     } 
    } 
} 

이 구현은 thread 세이프 한 번 초기화됩니다. 그러나 초기화 자체가 다른 소비자 스레드에 의해 경쟁되면 경주 위험이 발생할 수 있으므로 후자 스레드가 내부 Queue<T>이 초기화되기 전에 인스턴스에 액세스하게됩니다. 고안된 예 :

ThreadSafeQueue<int> queue = null; 

Parallel.For(0, 10000, i => 
{ 
    if (i == 0) 
     queue = new ThreadSafeQueue<int>(); 
    else if (i % 2 == 0) 
     queue?.Enqueue(i); 
    else 
    { 
     int item = -1; 
     if (queue?.TryDequeue(out item) == true) 
      Console.WriteLine(item); 
    } 
}); 

위의 코드는 일부 숫자가 누락 될 수 있습니다. 그러나 메모리 장벽이없는 경우 Enqueue 또는 TryDequeue 시간이 될 때까지 내부 Queue<T>이 초기화되지 않아서 NullReferenceException (또는 다른 별난 결과)이 표시 될 수 있습니다.

생성자 끝에 메모리 장벽을 포함시키는 것은 스레드 안전 클래스의 책임입니까, 아니면 클래스의 인스턴스화와 다른 스레드에 대한 가시성 사이에 메모리 장벽을 포함해야하는 소비자입니까? 스레드로부터 안전한 것으로 표시된 클래스에 대한 .NET Framework의 규칙은 무엇입니까?

편집 : 고급 스레드 주제이므로 일부 의견의 혼동을 이해합니다. 인스턴스는 적절한 동기화없이 다른 스레드에서 액세스하는 경우 하프 베이크로 나타납니다. 이 항목은 double-checked locking의 컨텍스트에서 광범위하게 논의되며, 이는 메모리 장벽을 사용하지 않고 ECMA CLI 사양 (예 : volatile)없이 손상되었습니다. Jon Skeet 당 :

자바 메모리 모델은 새 개체에 대한 참조가 예를에 할당되기 전에 생성자가 완료되었는지 확인하지 않습니다. Java 메모리 모델은 버전 1에 대한 재 작업을 수행했습니다.5이지만, 이중 체크 락은 휘발성 변수 (C#에서와 같이 ) 없이도 여전히 깨지게됩니다.

메모리 장벽이 없어도 ECMA CLI 사양에서 오류가 발생했습니다. .NET 2.0 메모리 모델 (ECMA 사양보다 강력 함)에서는 안전 할 수 있지만, 특히 안전성에 대해 의심의 여지가있는 경우 해당 의미에 의존하지 않을 수 있습니다.

+1

언급 한 'ConcurrentQueue '의 소스 코드는 생성자에 보호 기능이 없습니다. 당신이 원하는 것을 만드십시오. http://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentQueue.cs,18bcbcbdddbcfdcb –

+0

초기화를 스레드로부터 안전하게 만드는 Lazy 을 사용하여 소비자를 초기화하는 방법은 어떻습니까? :) – user3185569

+1

실제로 비동기 호출을 생성자가 없다면, 참조가 인스턴스가 생성되기 전에 인스턴스를 참조하도록 설정할 수 있습니까? – Uueerdo

답변

1

Lazy<T>은 스레드 안전 초기화에 매우 적합한 옵션입니다. 나는 그를 제공하기 위해 소비자에게 왼쪽해야한다고 생각 :

var queue = new Lazy<ThreadSafeQueue<int>>(() => new ThreadSafeQueue<int>()); 

Parallel.For(0, 10000, i => 
{ 

    else if (i % 2 == 0) 
     queue.Value.Enqueue(i); 
    else 
    { 
     int item = -1; 
     if (queue.Value.TryDequeue(out item) == true) 
      Console.WriteLine(item); 
    } 
}); 
+1

+1 : 이것은 내 불확실성을 감안할 때 현재하고있는 일입니다. 하지만 코드가 소비자의 스레드 동기화없이 작동 할 것으로 예상되는지 여부를 알고 싶습니다. 다시 말해, 스레드 안전 클래스를 구현하는 경우, 소비자가 여전히'Lazy '을 사용해야한다면 클래스를 "thread-safe"로 호출 할 수 있어야합니까? – Douglas

+0

@Douglas는 편집 된 답변을 확인합니다. – user3185569

+1

편집 내용을 이해할 수 없습니다. 원래 코드는 초기화를 두 번 실행하지 않았을 것입니다. – Douglas

0

아니, 당신은 생성자에서 메모리 장벽이 필요하지 않습니다. 당신의 가정은 창조적 인 생각을 보여 주지만 잘못된 것입니다. 어떤 스레드도 queue의 반 백업 인스턴스를 얻을 수 없습니다. 새 참조는 초기화가 완료 될 때만 다른 스레드에 "표시"됩니다. thread_1이 queue을 초기화하는 첫 번째 스레드라고 가정합니다. 주 코드에서 queue의 참조가 여전히 null입니다. thread_1이 생성자 코드를 가지고있을 때만 참조를 할당합니다.

아래의 설명과 OP 정교한 질문을 참조하십시오.

당신의 간단한 질문에 대한 대답에서
+1

불행히도 ECMA CLI 메모리 모델의 복잡함을 놓치고 있다고 생각합니다. Queue의 반 - 구운 인스턴스를 다른 쓰레드가 볼 수있다. – Douglas

+1

이 답변은 불행히도 기본적으로 희망적인 생각입니다. 동기화가 없다고 가정합니다. – hvd

+1

@Douglas 나는 그것에 대해 생각하지 않았다는 것을 인정해야합니다. 그러나 System.Collections에서 메모리 장벽에 대한 어떠한 증거도 찾을 수 없습니다.** 정의에 따라 ** 스레드 안전 인 동시 클래스 클래스입니다. 주제가 스레드 안전 정의를 새 영역으로 확장합니다. 그리고 그것은 꽤 멋져요 :) –

0

: 당신의 코드가 queue 전에 queue.Enqueue(5)에 값이 호출 할 수 있다는 것을 확실히 가능하다

ConcurrentQueue<int> queue = null; 

Parallel.Invoke(
    () => queue = new ConcurrentQueue<int>(), 
    () => queue?.Enqueue(5)); 

하지만 생성자 내에서 당신을 방지 할 수 아무것도 아니다 Queue입니다. queue에는 생성자가 완료 될 때까지 새 인스턴스에 대한 참조가 실제로 할당되지 않습니다.

+0

두 번째 델리게이트가'queue'가 할당되기 전에 실행된다면 아무 일도하지 않을 것입니다; null 조건 연산자'? .'는'NullReferenceException'을 방지합니다. 그러나 두 번째 델리게이트가'queue'가 할당 된 후에 실행되고 생성자가 다른 스레드의 관점에서 실행을 완료하기 전에 두 번째 델리게이트가 실행되면 어떻게되는지를 묻습니다. – Douglas

+0

아, 그래, 나는 그 안에 '?'를 놓쳤다. 내 이해는 그것이 일어날 수 없다는 것이다. 생성자가 완료 될 때까지 참조가 지정되지 않습니다. –

+1

'큐는 실제로 생성자가 완료 될 때까지 새 인스턴스에 대한 참조가 할당되지 않습니다. '이것은 단일 스레드 프로그램에 해당됩니다. 멀티 스레드 환경에서는 이러한 작업이 순서없이 수행되는 것을 관찰 할 수 있습니다. – Servy

1

생성자 내부에 기록 된 모든 최종 필드 Java에 생성자가 존재 후 작성이 개 울타리가 것이라고 관련없는,하지만 여전히 흥미 : StoreStoreLoadStore - 그 기준 스레드 안전을 게시 할 것입니다.

+0

감사합니다. 알아 둘만한! – Douglas