Microsoft MVP성태의 닷넷 이야기
.NET Framework: 209. AutoReset, ManualReset, Monitor.Wait의 차이 [링크 복사], [링크+제목 복사],
조회: 21654
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13240정성태2/1/20234165디버깅 기술: 187. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.Web.HttpException
13239정성태2/1/20233838디버깅 기술: 186. C# - CacheDependency의 숨겨진 예외 - System.Web.HttpException
13238정성태1/31/20235946.NET Framework: 2092. IIS 웹 사이트를 TLS 1.2 또는 TLS 1.3 프로토콜로만 운영하는 방법
13237정성태1/30/20235631.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/20235162개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/20234742개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/20235851개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법 [1]
13233정성태1/28/20237234오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/20234944스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/20233939오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/20234303개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/20235295.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/20235398.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/20235085개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요? [1]
13226정성태1/23/20234770.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/20233955개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/20234369Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
13223정성태1/20/20234545오류 유형: 838. RDP 연결 오류 - The two computers couldn't connect in the amount of time allotted
13222정성태1/20/20234252개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
13221정성태1/19/20234459Linux: 57. C# - 리눅스 프로세스 메모리 정보파일 다운로드1
13220정성태1/19/20234540오류 유형: 837. NETSDK1045 The current .NET SDK does not support targeting .NET ...
13219정성태1/18/20234155Windows: 220. 네트워크의 인터넷 접속 가능 여부에 대한 판단 기준
13218정성태1/17/20234078VS.NET IDE: 178. Visual Studio 17.5 (Preview 2) - 포트 터널링을 이용한 웹 응용 프로그램의 외부 접근 허용
13217정성태1/13/20234675디버깅 기술: 185. windbg - 64비트 운영체제에서 작업 관리자로 뜬 32비트 프로세스의 덤프를 sos로 디버깅하는 방법
13216정성태1/12/20234932디버깅 기술: 184. windbg - 32비트 프로세스의 메모리 덤프인 경우 !peb 명령어로 나타나지 않는 환경 변수
13215정성태1/11/20236581Linux: 56. 리눅스 - /proc/pid/stat 정보를 이용해 프로세스의 CPU 사용량 구하는 방법 [1]
... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...