Microsoft MVP성태의 닷넷 이야기
.NET Framework: 209. AutoReset, ManualReset, Monitor.Wait의 차이 [링크 복사], [링크+제목 복사]
조회: 21579
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
(시리즈 글이 4개 있습니다.)
.NET Framework: 209. AutoReset, ManualReset, Monitor.Wait의 차이
; https://www.sysnet.pe.kr/2/0/1015

.NET Framework: 1120. C# - BufferBlock<T> 사용 예제
; https://www.sysnet.pe.kr/2/0/12845

.NET Framework: 1172. .NET에서 Producer/Consumer를 구현하는 기초 인터페이스 - IProducerConsumerCollection<T>
; https://www.sysnet.pe.kr/2/0/12993

.NET Framework: 1173. .NET에서 Producer/Consumer를 구현한 BlockingCollection<T>
; https://www.sysnet.pe.kr/2/0/12995




AutoReset, ManualReset, Monitor.Wait의 차이

대개의 경우, Producer/Consumer 큐를 구현한다면 다음과 같은 식으로 구조를 잡을 것입니다.

threadpool_exit_event_1.png

즉, Consumer에 해당하는 스레드들은 특정 이벤트를 대기하고 있다가 Producer가 WorkItem을 큐잉하고 이벤트 Signal을 해주면 깨어나서 큐에 담겨 있던 WorkItem을 꺼내서 작업을 하는 구조입니다.

이런 환경에서, AutoReset/ManualReset/Monitor.Wait의 차이를 알아보기 위해 Consumer 용도로 사용되는 스레드들을 모두 '자발적인 종료'가 되도록 해보겠습니다.




1. AutoReset으로 이벤트를 구현하는 경우


Consumer 스레드들이 사용하는 대기 개체를 AutoReset 유형의 이벤트로 구현하는 경우를 가정해 보겠습니다. 스레드 함수들은 다음과 같이 대기할 것이고,

EventWaitHandle waitEvent = new EventWaitHandle(false, EventResetMode.AutoReset);

void ThreadRun(object state)
{
    while (true)
    {
        waitEvent.WaitOne();
        if (threadAllExit == true)
        {
            break;
        }

        WaitCallback callback = workList.Dequeue();
        callback();
    }
}

모든 스레드에 대한 '자발적인 종료'를 시키기 위해 waitEvent.Set을 호출하게 될 것입니다.

threadAllExit = true;
waitEvent.Set();

위의 코드 만으로 Consumer 스레드들이 모두 종료하게 될까요? 물론 아닙니다. waitEvent.Set은 WaitOne으로 대기하고 있는 단 하나의 스레드만을 깨울 뿐이기 때문에, 만약 5개의 Consumer 스레드들이 있다면 이런 경우 4개는 그대로 WaitOne 상태에 머물게 됩니다.

오호... 그렇다면 waitEvent.Set()을 스레드 숫자만큼 루프를 도는 것은 어떨까요?

threadAllExit = true;
for (int i = 0; i < threadCount; i ++)
{
    waitEvent.Set();
}

이론상으로 보면, AutoReset 이벤트는 한번 Signaled 상태가 되면 대기 중인 스레드가 "깨어나는 동작"이 있어야만 다시 Unsignaled 상태로 돌아가게 되므로 5개의 스레드는 모두 깨어나야 하는 것이 정상입니다.

하지만, Set 메서드를 연이어 실행하는 것으로 인해 최악의 경우 모든 Consumer 스레드들이 WorkItem을 처리중에 있다고 가정한다면 연속적인 5번의 Set 호출은 결국 한번 호출한 것과 같은 효과만 있을 뿐이어서 4개의 스레드가 WaitOne 호출로 인해 대기 상태에 빠지게 됩니다.

개인적인 의견으로, AutoReset 이벤트로는 여기까지가 최선인 것 같습니다. 연이은 Set 메서드 호출 사이에 예상되는 WorkItem 처리 시간만큼의 delay를 준다거나 하는 식으로 개선할 수는 있겠지만, 100%의 신뢰성을 얻으려면 그러한 처리 이후에도 살아있는 스레드가 있다면 아예 강제종료를 해주어야 합니다.

(예제 코드: 첨부 파일의 "WindowsFormsAppliation1.sln" 프로젝트)




2. ManualReset으로 이벤트를 구현하는 경우


다들 아시는 것처럼, ManualReset 이벤트는 명시적인 Reset을 해주지 않는 한 계속해서 Signaled 상태로 머물러 있게 됩니다.

그래서, Consumer 스레드는 WaitOne에서 깨어난 후 다음과 같이 Reset을 호출해 주어야 합니다.

EventWaitHandle waitEvent = new EventWaitHandle(false, EventResetMode.AutoReset);

void ThreadRun(object state)
{
    while (true)
    {
        waitEvent.WaitOne();
        if (threadAllExit == true)
        {
            break;
        }

        waitEvent.Reset();

        WaitCallback callback = workList.Dequeue();
        callback();
    }
}

이러한 ManualReset 동작 방식으로 인해, 다음과 같이 이벤트 개체를 signal 상태로 한 번만 상태 변경을 해주면 모든 스레드들이 쉽게 깨어나게 됩니다.

threadAllExit = true;
waitEvent.Set();

하지만, Consumer 스레드가 다중으로 깨어날 수 있다는 약간의 부작용이 있습니다. Reset이 될 때까지 시간이 걸리기 때문에 그사이 대기 중이던 다른 스레드들이 역시 signaled 된 이벤트 개체로 인해 깨어날 수 있기 때문입니다.

따라서, 만약 100개의 스레드 풀을 운영하고 있는데 Reset 되는 시간까지의 예측할 수 없는 지연 시간에 따라 최악의 경우 100개의 스레드 모두 깨어날 수 있습니다. 만약, 이런 일이 빈번하게 일어나는 구조라면 ManualReset 방식을 쓰기에는 다소 껄끄러울 수 있습니다.

즉, ManualReset 이벤트는 WaitOne으로 해당 이벤트를 기다리고 있는 스레드를 모두 깨우게 됩니다. 반면 WaitOne 이외의 처리를 하던 스레드들은 Set/Reset의 시간 차 안에 WaitOne 호출이 이뤄지느냐에 따라 동작 여부가 갈리기도 합니다.

(예제 코드: 첨부 파일의 "WindowsFormsAppliation2.sln" 프로젝트)




3. Monitor.Wait/Pulse로 구현하는 경우


마지막으로 살펴볼 Monitor.Wait/Pulse입니다. EventWaitHandle 사용법과 비교해서 다소 복잡한데요. 혹시나 이 방법에 대해 들어보신 적이 없다면 다음의 글을 한번 읽고 진행하는 것이 좋겠습니다.

Nonblocking Synchronization 
; http://www.albahari.com/threading/part4.aspx

그럼, 시작해 볼까요?

우선 Consumer 스레드 먼저 살펴보면 아래와 같이 lock과 Monitor.Wait을 병행해 주어야 합니다.

object waitEvent = new object();

void ThreadRun(object state)
{
    while (true)
    {
        lock (waitEvent)
        {
            Monitor.Wait(waitEvent);
        }

        if (threadAllExit == true)
        {
            break;
        }

        WaitCallback callback = workList.Dequeue();
        callback();
    }
}

종료를 하려면 Monitor.Pulse를 다음과 같이 호출해 주어야 하는데요.

threadAllExit = true;

lock (waitEvent)
{
    Monitor.Pulse(waitEvent);
}

위와 같이 하면, "대기 중에 있는 스레드" 하나만 깨어납니다. 따라서, "대기 중에 있는 모든 스레드"를 깨어나게 하려면 다음과 같이 해주면 됩니다.

threadAllExit = true;

lock (waitEvent)
{
    Monitor.PulseAll(waitEvent);
}

하지만, PulseAll 메서드가 Signaled 상태로 계속 머물게 하지는 않습니다. MSDN의 설명에 의하면,

The Monitor class does not maintain state indicating that the Pulse method has been called. Thus, if you call Pulse when no threads are waiting, the next thread that calls Wait blocks as if Pulse had never been called. If two threads are using Pulse and Wait to interact, this could result in a deadlock. Contrast this with the behavior of the AutoResetEvent class: If you signal an AutoResetEvent by calling its Set method, and there are no threads waiting, the AutoResetEvent remains in a signaled state until a thread calls WaitOne, WaitAny, or WaitAll. The AutoResetEvent releases that thread and returns to the unsignaled state.


Monitor.Wait/Pulse 모델이 AutoResetEvent 유형보다 상태 관리면에서 보면 부실한 것을 볼 수 있습니다. 결국 적절한 해결 방법이 될 수 없습니다.

(예제 코드: 첨부 파일의 "WindowsFormsAppliation3.sln" 프로젝트)




4. 개선 방법 = AutoReset + ManualReset


결국, 이렇게 해서 Consumer/Producer 방식에서 만족스럽게 사용할 만한 동기화 방식이 없다는 것을 알 수 있는데요. 그렇다고 포기할 수는 없지요. ^^ 다행히 이리저리 방법을 궁리하다 보면 답이 나오는데, 바로 AutoReset 이벤트와 ManualReset 이벤트를 조합하면 되는 것입니다.

ManualReset Event: 모든 스레드를 종료하는 신호로 사용
AutoReset Event: 작업 항목이 도착했음을 알리는 신호로 사용

이에 따라 다음과 같이 EventWaitHandle.Any와 적절하게 조합해서 사용하면 우리가 원하는 효과를 얻을 수 있습니다.

EventWaitHandle waitEvent = new EventWaitHandle(false, EventResetMode.AutoReset);
EventWaitHandle closeAllEvent = new EventWaitHandle(false, EventResetMode.ManualReset);
EventWaitHandle[] waitsEvent;

private void Form1_Load(object sender, EventArgs e)
{
    this.waitsEvent = new EventWaitHandle[] { waitEvent, closeAllEvent };
}

void ThreadRun(object state)
{
    while (true)
    {
        int result = EventWaitHandle.WaitAny(waitsEvent);

        if (result == 1) // when closeAllEvent signaled.
        {
            break;
        }

        // 작업 아이템을 받아서 동작
        WaitCallback callback = null;
        lock (lock_work)
        {
            callback = workList.Dequeue();
        }

        if (callback != null)
        {
            callback(null);
        }
    }
}

(예제 코드: 첨부 파일의 "WindowsFormsAppliation4.sln" 프로젝트)




5. 결론은... 차이점 이해!


생각보다 간단하지요! ^^

위의 예에서는 각각의 동기화 모델 간의 차이점을 설명하기 위해 'Consumer 스레드들의 자발적인 종료'라는 가정을 내세웠기 때문에 "AutoReset + ManualReset"이 적절하다고 결론이 나오긴 했지만, 여러분들이 만드는 Producer/Consumer 모델에서 반드시 이를 따라야 할 필요는 없기 때문에 상황에 따라 적절한 동기화 모델을 사용하시면 되겠습니다. 중요한 것은, '각각의 차이점을 이해하고 적용'하자는 것입니다.

(이 글의 다이어그램을 만들기 위해 제작된 vsd 원본 파일과 모든 소스 코드는 첨부된 압축 파일에 포함되어 있으며, 본문의 예제 소스에서 누락된 동기화 부분이 좀 더 들어가 있는 차이가 있습니다.)



[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/14/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13608정성태4/26/2024220닷넷: 2249. C# - 부모의 필드/프로퍼티에 대해 서로 다른 자식 클래스 간에 Reflection 접근이 동작할까요?파일 다운로드1
13607정성태4/25/2024216닷넷: 2248. C# - 인터페이스 타입의 다중 포인터를 인자로 갖는 C/C++ 함수 연동
13606정성태4/24/2024235닷넷: 2247. C# - tensorflow 연동 (MNIST 예제)파일 다운로드1
13605정성태4/23/2024511닷넷: 2246. C# - Python.NET을 이용한 파이썬 소스코드 연동파일 다운로드1
13604정성태4/22/2024579오류 유형: 901. Visual Studio - Unable to set the next statement. Set next statement cannot be used in '[Exception]' call stack frames.
13603정성태4/21/2024778닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동파일 다운로드1
13602정성태4/20/2024845닷넷: 2244. C# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)파일 다운로드1
13601정성태4/19/2024884닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/2024908닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/2024878닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/2024911닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드2
13597정성태4/15/2024894닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/20241077닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/20241060닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20241075닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241088닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241226C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241201닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241081Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241158닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241273닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신 [2]파일 다운로드1
13587정성태3/27/20241358오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241532Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241313Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241273개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...