Microsoft MVP성태의 닷넷 이야기
.NET Framework: 209. AutoReset, ManualReset, Monitor.Wait의 차이 [링크 복사], [링크+제목 복사],
조회: 28978
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 5개 있습니다.)
(시리즈 글이 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 원본 파일과 모든 소스 코드는 첨부된 압축 파일에 포함되어 있으며, 본문의 예제 소스에서 누락된 동기화 부분이 좀 더 들어가 있는 차이가 있습니다.)



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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/26/2025]

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

비밀번호

댓글 작성자
 




... [136]  137  138  139  140  141  142  143  144  145  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1654정성태3/19/201425183개발 환경 구성: 219. SOS.dll 확장 모듈을 버전 별로 구하는 방법 [4]
1653정성태3/13/201420015.NET Framework: 428. .NET Reflection으로 다차원/Jagged 배열을 구분하는 방법
1652정성태3/12/201421083VC++: 76. Direct Show를 사용하는 다른 프로그램의 필터 그래프를 graphedt.exe에서 확인하는 방법파일 다운로드1
1651정성태3/11/201424715.NET Framework: 427. C# 컴파일러는 변수를 초기화시키지 않을까요?
1650정성태3/6/201425500VC++: 75. Visual C++ 컴파일 오류 - Cannot use __try in functions that require object unwinding [1]파일 다운로드1
1649정성태3/5/201420154기타: 44. BTN 스토어 앱 개인정보 보호 정책 안내
1648정성태3/5/201420543개발 환경 구성: 218. 스토어 앱 인증 실패 - no privacy statement
1647정성태3/3/201421769오류 유형: 224. 스카이드라이브 비정상 종료 - Error 0x80040A41: No error description available
1646정성태3/3/201431022오류 유형: 223. Microsoft-Windows-DistributedCOM 10016 이벤트 로그 에러 [1]
1645정성태3/1/201420811기타: 43. 마이크로소프트 MVP들이 모여 전국 세미나를 엽니다.
1644정성태2/26/201427776.NET Framework: 426. m3u8 스트리밍 파일을 윈도우 8.1 Store App에서 재생하는 방법파일 다운로드1
1643정성태2/25/201423560오류 유형: 222. 윈도우 8 Store App - APPX1204 SignTool Error: An unexpected internal error has occurred [1]
1642정성태2/25/201428151Windows: 91. 한글이 포함된 사용자 프로파일 경로 변경 [2]
1641정성태2/24/201425043기타: 42. 클래스 설명 [5]
1640정성태2/24/201446017.NET Framework: 425. C# - VLC(ActiveX) 컨트롤을 레지스트리 등록 없이 사용하는 방법 [15]
1639정성태2/23/201421738기타: 41. BBS 스토어 앱 개인정보 보호 정책 안내
1638정성태2/18/201444375Windows: 90. 실행 파일로부터 관리자 요구 권한을 제거하는 방법(부제: 크랙 버전을 보다 안전하게 실행하는 방법) [8]
1637정성태2/14/201425504Windows: 89. 컴퓨터를 껐는데도 어느 순간 자동으로 켜진다면? - 두 번째 이야기
1636정성태2/14/201421387Windows: 88. Hyper-V가 설치된 컴퓨터의 윈도우 백업 설정
1635정성태2/14/201422316오류 유형: 221. SharePoint - System.InvalidOperationException: The farm is unavailable.
1634정성태2/14/201422462.NET Framework: 424. C# - CSharpCodeProvider로 컴파일한 메서드의 실행이 일반 메서드보다 더 빠르다? [1]파일 다운로드1
1633정성태2/13/201425333오류 유형: 220. 2014년 2월 13일 이후로 Visual Studio 2010 Macro가 동작하지 않는다면? [3]
1632정성태2/12/201443309.NET Framework: 423. C#에서 DirectShow를 이용한 미디어 재생 [2]파일 다운로드1
1631정성태2/11/201422332개발 환경 구성: 217. Realtek 사운드 장치에서 재생되는 오디오를 GraphEditor로 녹음하는 방법
1630정성태2/5/201422636개발 환경 구성: 216. Hyper-V에 올려진 윈도우 XP VM에서 24bit 컬러 및 ClearType 활성화하는 방법
1629정성태2/5/201432449개발 환경 구성: 215. DOS batch - 하나의 .bat 파일에서 다중 .bat 파일을 (비동기로) 실행하는 방법 [1]
... [136]  137  138  139  140  141  142  143  144  145  146  147  148  149  150  ...