오늘은 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);
}
}
'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 |