Microsoft MVP성태의 닷넷 이야기
.NET Framework: 209. AutoReset, ManualReset, Monitor.Wait의 차이 [링크 복사], [링크+제목 복사]
조회: 21585
글쓴 사람
정성태 (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)
13357정성태5/16/20233575.NET Framework: 2124. C# - Semantic Kernel의 Planner 사용 예제파일 다운로드1
13356정성태5/15/20233901DDK: 10. Device Driver 테스트 설치 관련 오류 (Code 37, Code 31) 및 인증서 관련 정리
13355정성태5/12/20233836.NET Framework: 2123. C# - Semantic Kernel의 ChatGPT 대화 구현 [1]파일 다운로드1
13354정성태5/12/20234095.NET Framework: 2122. C# - "Use Unicode UTF-8 for worldwide language support" 설정을 한 경우, 한글 입력이 '\0' 문자로 처리
13352정성태5/12/20233704.NET Framework: 2121. C# - Semantic Kernel의 대화 문맥 유지파일 다운로드1
13351정성태5/11/20234209VS.NET IDE: 185. Visual Studio - 원격 Docker container 내에 실행 중인 응용 프로그램에 대한 디버깅 [1]
13350정성태5/11/20233480오류 유형: 859. Windows Date and Time - Unable to continue. You do not have permission to perform this task
13349정성태5/11/20233791.NET Framework: 2120. C# - Semantic Kernel의 Skill과 Function 사용 예제파일 다운로드1
13348정성태5/10/20233689.NET Framework: 2119. C# - Semantic Kernel의 "Basic Loading of the Kernel" 예제
13347정성태5/10/20234082.NET Framework: 2118. C# - Semantic Kernel의 Prompt chaining 예제파일 다운로드1
13346정성태5/10/20233912오류 유형: 858. RDP 원격 환경과 로컬 PC 간의 Ctrl+C, Ctrl+V 복사가 안 되는 문제
13345정성태5/9/20235313.NET Framework: 2117. C# - (OpenAI 기반의) Microsoft Semantic Kernel을 이용한 자연어 처리 [1]파일 다운로드1
13344정성태5/9/20236491.NET Framework: 2116. C# - OpenAI API 사용 - 지원 모델 목록 [1]파일 다운로드1
13343정성태5/9/20234365디버깅 기술: 192. Windbg - Hyper-V VM으로 이더넷 원격 디버깅 연결하는 방법
13342정성태5/8/20234273.NET Framework: 2115. System.Text.Json의 역직렬화 시 필드/속성 주의
13341정성태5/8/20234007닷넷: 2114. C# 12 - 모든 형식의 별칭(Using aliases for any type)
13340정성태5/8/20234080오류 유형: 857. Microsoft.Data.SqlClient.SqlException - 0x80131904
13339정성태5/6/20234746닷넷: 2113. C# 12 - 기본 생성자(Primary Constructors)
13338정성태5/6/20234265닷넷: 2112. C# 12 - 기본 람다 매개 변수파일 다운로드1
13337정성태5/5/20234769Linux: 59. dockerfile - docker exec로 container에 접속 시 자동으로 실행되는 코드 적용
13336정성태5/4/20234577.NET Framework: 2111. C# - 바이너리 출력 디렉터리와 연관된 csproj 설정
13335정성태4/30/20234677.NET Framework: 2110. C# - FFmpeg.AutoGen 라이브러리를 이용한 기본 프로젝트 구성 - Windows Forms파일 다운로드1
13334정성태4/29/20234312Windows: 250. Win32 C/C++ - Modal 메시지 루프 내에서 SetWindowsHookEx를 이용한 Thread 메시지 처리 방법
13333정성태4/28/20233754Windows: 249. Win32 C/C++ - 대화창 템플릿을 런타임에 코딩해서 사용파일 다운로드1
13332정성태4/27/20233855Windows: 248. Win32 C/C++ - 대화창을 위한 메시지 루프 사용자 정의파일 다운로드1
13331정성태4/27/20233885오류 유형: 856. dockerfile - 구 버전의 .NET Core 이미지 사용 시 apt update 오류
1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...