Program/C#

C# 병렬처리2 _ Monitor

사막여유 2024. 11. 22. 23:07
728x90

 

오늘은 c#에 있는 병렬처리중 쓰레드 경합방지를 위한 Monitor에 대해서 알고보고자 합니다.

다른 경합방지를 위한 방법 중 lock 키워드에 대해 궁금하신 분들은 아래 링크를 먼저 봐주세요

https://opencv-master.tistory.com/177

 

C# 병렬처리2 _ lock

오늘은 c#에 있는 병렬처리중 쓰레드 경합방지를 위한 Lock에 대해서 알고보고자 합니다. Lock 키워드lock은 object 키워드와 함께 사용되는데간단하게 말하면 현재 1번 쓰레드가 사용하고있으면 2번

opencv-master.tistory.com

 

 

Monitor 클래스

Monitor 클래스는 lock 키워드보다 좀 더 섬세한 작업이 가능한데, Enter와 Exit 메서드를 통해 다른 쓰레드들이 언제부터 언제까지 대기해야 하는지 구체적인 타이밍을 지정해줄 수 있습니다.

Monitor 클래스에는 다음과 같은 메서드들이 있습니다.

 - Enter()
 - Exit()
 - TryEnter()
 - Wait()
 - Pulse()/PulseAll()

각각의 메서드의 역할에 대해서 알아보면 일단 Monitor 클래스를 사용하기 위한 가장 기본적인 메서드는
Enter()와 Exit() 메서드입니다.

사용하는 방식은 같은 메서드일때는 lock 키워드와 거의 동일합니다.

await Task.Run(() =>
{
    for (int i = 0; i < 5; i++)
    {
        int threadId = i;
        var task = Task.Run(async () =>
        {
            int depositAmount = 1000;// Random.Shared.Next(1000, 5001);

            try
            {
                Monitor.Enter(syncObject);

                decimal myBalance = balance;
                Debug.WriteLine($"스레드{threadId}: 현재 {myBalance}원, {depositAmount}원 입금 시도");

                await Task.Delay(Random.Shared.Next(50, 150));

                decimal newBalance = myBalance + depositAmount;
                Debug.WriteLine($"스레드{threadId}: {myBalance} + {depositAmount} = {newBalance} 계산됨");

                await Task.Delay(Random.Shared.Next(50, 150));

                balance = newBalance;
                Debug.WriteLine($"스레드{threadId}: {newBalance}를 balance에 저장. 현재 balance = {balance}");
            }
            finally
            {
                Monitor.Exit(syncObject);
            }
        });
        tasks.Add(task);
    }
});

 

위 코드블럭과 같이 try문 안에서는 Monitor.Enter() 메서드로 lock을 걸어준 뒤 
마지막 꼭 들어가야하는 finally문 안에서는 Monitor.Exit() 메서드를 실행하여 걸려있던 쓰레드lock을 풀어주는 구조입니다.

그런데 이렇게만 사용하면 사실 lock 키워드를 사용하는 것과 큰 차이가 없죠.
lock 키워드를 파고 들어가보면 Monitor 클래스의 Enter(), Exit() 메서드를 try-finally 블록으로 감싸서 구현한 것이기 때문입니다.

그래서 Monitor 클래스를 사용하는 이유는 TryEnter(), Wait(), Pulse() 메서드들을 사용해 조금 더 세밀한 제어가 가능하기 때문입니다.

 

Wati() 메서드는 쉽게 생각하면 아래 이미지와 같은 기능입니다.

    • 첫 번째 이미지 상황 (기본 접근 제어)
      • 여러 스레드(Thread1,2,3,4)가 하나의 프린터로 접근 시도
      • Monitor.Enter() 또는 lock 문을 사용하여 스레드들의 실행 순서 관리
      • 한 번에 하나의 스레드만 프린터 접근 가능
    • 두 번째 이미지 상황 (용지 부족)
      • 프린터에 용지가 없는 상황 발생
      • Thread1은 Monitor.TryEnter(timeout) 으로 일정 시간 대기 후 다른 프린터로 이동
      • Thread2,3,4는 Monitor.Wait() 를 호출하여 대기 상태로 전환
      • 이때 Wait()는 lock을 임시로 해제하여 다른 작업 가능하게 함
    • 세 번째 이미지 상황 (용지 보충 후)
      • 프린터에 용지가 보충되면 Monitor.Pulse() 또는 Monitor.PulseAll() 호출
      • 대기 중이던 Thread2가 깨어나서 실행(Run) 상태로 전환
      • Thread3,4는 계속 Wait() 상태로 대기
      • Thread1은 다른 프린터에서 계속 작업 진행

 

즉, Wait() 는 매서드명 그대로 사용하던 스레드를 대기상태로 전환하여 특정 조건을 만족할 때까지리소스 사용을 일시적으로 중단하는 매서드입니다.

그리고 해당 Wait 매서드가 "얼음" 이 되면 "땡" 하고 풀어주는 매서드가 Pulse() 매서드인 것이죠.

 

private async void BtnPrintWithWait_Click(object sender, EventArgs e)
{
    // Wait/Pulse 인쇄: 용지 부족시 대기하거나 다른 프린터 시도
    var tasks = new List<Task>();
    Log("=== Wait/Pulse 인쇄 시작 ===");

    for (int i = 0; i < 5; i++)
    {
        int docId = i;
        var task = Task.Run(() =>
        {
            bool printed = false;
            bool triedPrinter2 = false;

            while (!printed)
            {
                lock (syncObject)
                {
                    var currentPrinter = triedPrinter2 ?
                        printers["Printer2"] : printers["Printer1"];

                    if (currentPrinter.PaperCount == 0)
                    {
                        if (!triedPrinter2)
                        {
                            // 첫 번째 프린터 용지 부족시 다른 프린터 시도
                            Log($"문서{docId}: 프린터1 용지 부족. 5초간 대기...");
                            if (!Monitor.Wait(syncObject, 5000))
                            {
                                Log($"문서{docId}: 프린터1 대기 시간 초과. 프린터2 시도");
                                triedPrinter2 = true;
                                continue;
                            }
                        }
                        else
                        {
                            // 두 번째 프린터도 용지 부족시 대기
                            Log($"문서{docId}: 프린터2도 용지 부족. 대기...");
                            Monitor.Wait(syncObject);
                            continue;
                        }
                    }

                    // 인쇄 실행
                    currentPrinter.PaperCount--;
                    printed = true;
                    Log($"문서{docId}: {currentPrinter.Name}에서 인쇄 완료 " +
                        $"(남은 용지: {currentPrinter.PaperCount}장)");
                }
            }
        });
        tasks.Add(task);
        Thread.Sleep(100);  // 문서 간 간격
    }

    await Task.WhenAll(tasks);
}
private void BtnAddPaper_Click(object sender, EventArgs e)
{
    lock (syncObject)
    {
        // 첫 번째 프린터에 용지 보충
        printers["Printer1"].PaperCount += 5;
        Log($"프린터1 용지 5장 보충됨 (현재 {printers["Printer1"].PaperCount}장)");

        // 대기 중인 모든 인쇄 작업에 알림
        Monitor.PulseAll(syncObject);
    }
}

 

728x90

'Program > C#' 카테고리의 다른 글

C# _ Interface  (0) 2024.11.24
C# 병렬처리3 _ Mutex  (0) 2024.11.23
C# 병렬처리1 _ lock  (0) 2024.11.20
C# Bitmap  (1) 2024.11.18
C# 얕은복사, 깊은복사  (1) 2024.11.14