2012-12-27 1 views
0

BlockingCollection (코드는 아래 참조)을 기반으로 한 생산자 고객 패턴을 사용하는 간단한 로거가 있습니다.BlockingCollection의 IDisposable 대 생산자 소비자

public class Logger 
{ 
    public Logger() 
    { 
     _messages = new BlockingCollection<LogMessage>(int.MaxValue); 
     _worker = new Thread(Work) {IsBackground = true}; 
     _worker.Start(); 
    } 

    ~Logger() 
    { 
     _messages.CompleteAdding(); 
     _worker.Join();     // Wait for the consumer's thread to finish. 
     //Some logic on closing log file 
    } 

    /// <summary> 
    /// This is message consumer thread 
    /// </summary> 
    private void Work() 
    { 
     while (!_messages.IsCompleted) 
     { 
      //Try to get data from queue 
      LogMessage message; 
      try 
      { 
       message = _messages.Take(); 
      } 
      catch (ObjectDisposedException) { break; } //The BlockingCollection(Of T) has been disposed. 
      catch(InvalidOperationException){ continue; } //the BlockingCollection(Of T) is empty and the collection has been marked as complete for adding. 

      //... some simple logic to write 'message' 
     } 
    } 
} 

문제는 응용 프로그램이 즉시 종료되지 않는다는 것입니다. 응용 프로그램을 끝내는 데 20-40 초가 걸리고 디버거를 중간에 멈 추면 다음과 같이 표시됩니다.
1. GC.Finalize 스레드가 _worker.Join();에 설정됩니다.
2. _worker 스레드가 _messages.Take()에 있습니다.

_messages.Take()가 _messages.CompleteAdding(); 그러나 그렇지 않은 것처럼 보입니다.

이 마무리에서 어떤 문제가 있으며이 상황에서 작업자 스레드를 더 잘 마무리하는 방법은 무엇입니까?

P. _worker.Join()을 간단히 놓을 수는 있지만 Work()는 닫힌 파일에 무언가를 쓸 수 있습니다. 내 말은, 이것은 동시에 비 결정적인 상황입니다.

나는 ~ 로거()를 닫습니다()와 어떤 점에서 그것을 호출로 변경 한 개념 증명으로
업데이트
. 즉시 로거를 닫습니다. 그래서 _messages.Take()는 _messages.CompleteAdding() 바로 다음에 끝날 것입니다.

~ Logger에서 20-40 초 지연에 대한 유일한 설명은 GC 스레드의 높은 우선 순위에서 볼 수 있습니다. 다른 설명이있을 수 있습니까?

+0

예, GC가 공간을 확보 할 필요가 없기 때문에 마무리 도구가 오랫동안 호출되지 않았기 때문에 지연이 발생했습니다. – ricovox

+0

@ricovox 정말 도와 주셔서 감사합니다! 그러나, 제발, 더 \t stackoverflow 질문에 사려 깊어. 귀하의 의견은 묻는 질문과 아무 관련이 없습니다. – MajesticRa

답변

3

C#, 종결 자 (소멸자라고도 함)은 비 결정적입니다. 즉, 호출시기 또는 순서를 예측할 수 없습니다. 예를 들어, 코드에서 _worker의 종료자가 이전에 전에 Logger의 최종자를 완전히 사용할 수 있습니다. 따라서 다른 관리되는 리소스의 finalizer가 이미 완료되어 참조가 유효하지 않기 때문에 finalizer 내부의 관리 대상 객체 (예 : FileStream 등)에 액세스해서는 안됩니다. 또한 GC가 컬렉션이 필요하다고 판단한 후에 (추가 메모리가 필요하기 때까지) finalizer가 호출됩니다. 귀하의 경우, GC는 필요한 수집을하기 전에 20-40 초가 걸릴 것입니다.

파이널 라이저를 없애고 대신 IDisposable 인터페이스를 사용하십시오 (선택적으로 더 나은 가독성을 제공하는 Close() 메소드 사용).

그러면 더 이상 필요하지 않으면 logger.Close()으로 전화하면됩니다. 당신이 (당신이 P를 사용하는 경우, 예를 들어,/호출 WinAPI를 기능 등) 호출 정리 관리되지 않는 자원이있을 때

void IDisposable.Dispose() 
{ 
    Close(); 
} 

void Close() 
{ 
    _messages.CompleteAdding(); 
    _worker.Join(); // Wait for the consumer's thread to finish. 
    //Some logic on closing log file 
} 

는 일반적으로 만 종료자를 사용합니다. .Net 클래스 만 사용하는 등의 경우에는 사용할 필요가 없습니다. ID는 거의 결정적 정리를 제공하기 때문에 항상 좋은 선택입니다.

파이 나라 대 소멸자에 대한 자세한 내용을 보려면 여기를 살펴 : 나는 당신의 코드를 만들 것 What is the difference between using IDisposable vs a destructor in C#?

또 다른 변화는 테이크 대신 TryTake를 사용하고 있습니다. 이는 콜렉션이 비어 있고 CompleteAdding이 호출 될 때 예외를 throw하지 않기 때문에 try/catch의 필요성을 제거합니다.단순히 거짓을 반환합니다.

private void Work() 
{ 
    //Try to get data from queue 
    LogMessage message; 
    while (_messages.TryTake(out message, Timeout.Infinite)) 
     //... some simple logic to write 'message'  
} 

코드에서 당신이 잡아 두 가지 예외 여전히 같은 BlockingCollection의 기본 모음 (자세한 내용은 MSDN 참조)가 배치되고, 이후에 액세스하거나 수정하는 등의 다른 이유로 발생할 수 있습니다. 그러나 기본 함수 컬렉션에 대한 참조를 보유하지 않고 작업 함수가 완료되기 전에 BlockingCollection을 삭제하지 않으므로 해당 코드에서 둘 다 발생하지 않아야합니다. 그래도 그런 예외를 잡으려는 경우 try/catch 블록 을 while 루프의 외부에 둘 수 있습니다. 예외가 발생한 후에도 루프를 계속 수행하고 싶지 않을 것이기 때문입니다.

마지막으로, int.MaxValue를 콜렉션의 용량으로 지정하는 이유는 무엇입니까? 컬렉션에 많은 메시지를 정기적으로 추가 할 예정이 아니라면이 작업을 수행해서는 안됩니다. 다음과 같이

그래서 모두, 당신의 코드를 다시 작성합니다

public class Logger : IDisposable 
{ 
    private BlockingCollection<LogMessage> _messages = null; 
    private Thread _worker = null; 
    private bool _started = false; 

    public void Start() 
    { 
     if (_started) return; 
     //Some logic to open log file 
     OpenLogFile();  
     _messages = new BlockingCollection<LogMessage>(); //int.MaxValue is the default upper-bound 
     _worker = new Thread(Work) { IsBackground = true }; 
     _worker.Start(); 
     _started = true; 
    } 

    public void Stop() 
    { 
     if (!_started) return; 

     // prohibit adding new messages to the queue, 
     // and cause TryTake to return false when the queue becomes empty. 
     _messages.CompleteAdding(); 

     // Wait for the consumer's thread to finish. 
     _worker.Join(); 

     //Dispose managed resources 
     _worker.Dispose(); 
     _messages.Dispose(); 

     //Some logic to close log file 
     CloseLogFile(); 

     _started = false; 
    } 

    /// <summary> 
    /// Implements IDiposable 
    /// In this case, it is simply an alias for Stop() 
    /// </summary> 
    void IDisposable.Dispose() 
    { 
     Stop(); 
    } 

    /// <summary> 
    /// This is message consumer thread 
    /// </summary> 
    private void Work() 
    { 
     LogMessage message; 
     //Try to get data from queue 
     while(_messages.TryTake(out message, Timeout.Infinite)) 
      WriteLogMessage(message); //... some simple logic to write 'message' 
    } 
} 

을 당신이 볼 수 있듯이, I/비활성화 큐 처리를 가능하게 Start()Stop() 방법을 추가했다. 원하는 경우 생성자에서 Start()를 호출 할 수 있지만 일반적으로 생성자에서 비싼 연산 (예 : 스레드 생성)을 원하지는 않을 것입니다. Open/Close 대신 Start/Stop을 사용했는데, 이는 로거에 더 의미가있는 것처럼 보였으므로 개인적인 취향 일 뿐이므로 어느 한 쌍이라도 정상적으로 작동합니다. 앞서 언급했듯이 Stop 또는 Close 메서드를 사용할 필요조차 없습니다. 단순히 Dispose()를 추가하는 것만으로 충분하지만 일부 클래스 (예 : Stream 등)는 Dispose의 별칭으로 Close 또는 Stop을 사용하여 코드를 더 읽기 쉽게 만듭니다.

+0

답변 해 주셔서 감사합니다! 게시물을 업데이트하여 Close() 메소드를 직접 호출하는 효과를 확인할 수 있습니다. 그러나 문제는 여기서 좀 더 까다 롭습니다. 어떤 파이널 라이저가 어떤 시점에 도달했는지 예측하려고하지는 않습니다. 코드는 직선이며 디버거를 사용하는 것과 동일하게 작동합니다. 유일한 차이점은 GC 스레드에서 동일한 코드를 호출하는 데 20-40 초가 걸린다는 점입니다. IDisposable 구현 문제는이 로거가 일부 ServiceLocator를 통해 사용된다는 것입니다. 서비스 로케이터는 아이템을 폐기 할 시점을 결정해야합니까? 아마도 또 다른 질문입니다. – MajesticRa

+0

'ServiceLocator'를 살펴보고이 주석을 더 자세하게 만들 수 있는지 알아 보겠습니다.하지만 응용 프로그램에서이 Logger 클래스의 전체 범위를 너무 많이 모르는 채로 Logger를 제외하고는 너무 많이 말할 수는 없습니다 사용을 마친 후에는 폐기해야합니다. 응용 프로그램이 닫히는 경우라면 괜찮습니다. 'Main()'함수의 마지막이나 Exit 나 AppDomain ProcessExit 이벤트와 같은 응용 프로그램 이벤트에서 호출 할 수 있습니다. – ricovox

+0

안녕하세요! 업데이트 해 주셔서 감사합니다. 1. Finalizer vs IDisposable에 대해 읽었습니다. 나는 무역을 알고있다. 2. Take 대신 TryTake를 사용하십시오. 기다려. TryTake는 CPU를 낭비합니다. 시도 해봐. 문서를 읽으십시오. 3. Start()와 Stop()은이 경우 좋지 않습니다. 새로운 스레드를 시작하기 때문에 Stop() 및 새 Start()가 호출되는 동안 메시지가 계속 쓰여질 수 있습니다. _started는 울타리가 있어야합니다. 그것에 대해 읽어보십시오. 따라서 안전하게 구현하려는 경우 복잡성이 커집니다. 4. 두 가지 예외 OCCURE를 수시로 포착합니다. 그리고 로거는 유일한 생산자 소비자가 아닙니다. – MajesticRa