오늘은 c#에 있는 병렬처리중 쓰레드 경합방지를 위한 Lock에 대해서 알고보고자 합니다.
Lock 키워드
lock은 object 키워드와 함께 사용되는데
간단하게 말하면 현재 1번 쓰레드가 사용하고있으면 2번,3번,4번 등등의 다른쓰레드에서는 접근할 수 없도록
키워드 그대로 lock을 걸어버리는 것입니다.
- Lock이라는 키워드는 가장 단순하게 그리고 직관적으로 사용할 수 있는 키워드입니다.
- Lock 블록을 빠져나올 때 자동으로 Lock이 해제되기 때문에 사용하기 정말 편하죠.
- 예외가 발생하더라도 안전하게 잠금이 해제됩니다.
사용 방법에 대한 예시를 한번 보겠습니다.
예를들어 여러명이 한개의 통장에 돈을 넣는 상황을 가정해보겠습니다.
첫번째로 lock 키워드 없이 여러명이 한번에 병렬로 잔액을 넣게되는 메서드입니다.
private async void BtnDepositWithoutLock_Click(object sender, EventArgs e)
{
btnDepositWithoutLock.Enabled = false;
btnDepositWithLock.Enabled = false;
await Task.Run(() =>
{
Parallel.For(0, 5, _ =>
{
// Lock 없이 입금
decimal temp = balance;
Thread.Sleep(100); // 처리 시간 시뮬레이션
temp = temp + 1000;
balance = temp;
});
});
lblBalance.Text = $"잔액: {balance}원";
btnDepositWithoutLock.Enabled = true;
btnDepositWithLock.Enabled = true;
}
두번째로는 lock 키워드를 사용해서 여러명이 한번에 병렬로 잔액을 넣게되는 메서드입니다.
private async void BtnDepositWithLock_Click(object sender, EventArgs e)
{
btnDepositWithoutLock.Enabled = false;
btnDepositWithLock.Enabled = false;
await Task.Run(() =>
{
Parallel.For(0, 5, _ =>
{
// Lock을 사용하여 입금
lock (lockObject)
{
decimal temp = balance;
Thread.Sleep(100); // 처리 시간 시뮬레이션
temp = temp + 1000;
balance = temp;
}
});
});
lblBalance.Text = $"잔액: {balance}원";
btnDepositWithoutLock.Enabled = true;
btnDepositWithLock.Enabled = true;
}
Parallel 클래스가 뭔지 잘 모르신다면 이 전 블로그 글을 참고해주세요
https://opencv-master.tistory.com/174
C# 병렬처리1 _ Parallel
오늘은 C#에 있는 병렬처리 Parallel 클래스에 대해서 알아보고자 합니다.현재 제가 하고 있는 프로젝트 중 그룹으로 묶인 도형을 Binary 처리된 이미지에 맞게Resize, Move, Rotate하는 메서드를 구현해
opencv-master.tistory.com
위 2개의 메서드를 실제로 실행해보면 아래 영상과 같은 결과를 얻을 수 있습니다.
위 영상을 보며 알 수 있는 점은 첫번째 lock키워드가 없이 Parallel로 병렬처리한 메서드는
아직 변수가 0인 상태에 여러개의 쓰레드들이 접근해서 읽어갑니다.
아래와같은 이미지가 되는거죠
그리고 그 결과 결국 최종적으로 "1000" 이라는 값이 반환될 수 밖에 없는 것이죠.
그런데 여기서 lock 키워드로 울타리를 만들어버리면 다른 쓰레드들은 순차적으로 차례를 기다리면
1000이 더해진 이후 다시 순차적으로 1000을 더하고 다시 1000을 더하는 방식인겁니다.
그런데 저는 좀 더 깊게, 본질적으로 생각해봤습니다.
lock이 없는 경우에 예를들어 5개의 쓰레드가 위와같이 하나의 메서드를 실행합니다.
그런데 3번쓰레드가 2번 쓰레드보다 늦게 시작하여 1000이 더해진 상태로 시작했다고 가정합니다.
그러면 1,2,4,5번의 쓰레드의 결과값은 1000이지만 3번 쓰레드는 2000입니다.
이런 상황에서는 어떠한 기준으로 몇번째 쓰레드의 값을 결과값으로 사용하게 될까요??
이를 알아보기 위해서 다음과 같은 코드를 작성해보겠습니다
private async void BtnDepositWithoutLock_Click(object sender, EventArgs e)
{
balance = 0;
Debug.WriteLine($"시작 balance = {balance}");
// 모든 작업을 담을 리스트
var tasks = new List<Task>();
await Task.Run(() =>
{
for (int i = 0; i < 5; i++)
{
int threadId = i; // 현재 스레드 ID 캡처
var task = Task.Run(async () =>
{
// 랜덤 금액 생성 (1000~5000 사이)
int depositAmount = Random.Shared.Next(1000, 5001);
// 1. 읽기
decimal myBalance = balance;
Debug.WriteLine($"스레드{threadId}: 현재 {myBalance}원, {depositAmount}원 입금 시도");
await Task.Delay(Random.Shared.Next(50, 150)); // 랜덤 지연
// 2. 계산
decimal newBalance = myBalance + depositAmount;
Debug.WriteLine($"스레드{threadId}: {myBalance} + {depositAmount} = {newBalance} 계산됨");
await Task.Delay(Random.Shared.Next(50, 150)); // 랜덤 지연
// 3. 쓰기
balance = newBalance;
Debug.WriteLine($"스레드{threadId}: {newBalance}를 balance에 저장. 현재 balance = {balance}");
});
tasks.Add(task);
}
});
// 모든 작업이 완료될 때까지 대기
await Task.WhenAll(tasks);
Debug.WriteLine($"최종 balance = {balance}");
lblBalance.Text = $"잔액: {balance}원";
}
결과를 먼저 보게되면
와 같이 나오는 결과를 볼 수 있습니다.
즉, 쓰레드가 경쟁상태 ( Race Condition ) 에 들어가게되면 가장 마지막으로 메모리에 쓰기 작업을 수행한 쓰레드의 값이 결과값이 되는 것입니다.
이렇게 여러개의 쓰레드가 경쟁상태에 들어가게되면 어떤 결과값이 출력될지 모르는 문제 때문에
병렬처리 중에도 출력(결과)를 동기적으로 제어해야하는 상황에서는 lock, Monitor, Mutex를 사용하는 것이죠.
'Program > C#' 카테고리의 다른 글
C# 병렬처리3 _ Mutex (0) | 2024.11.23 |
---|---|
C# 병렬처리2 _ Monitor (0) | 2024.11.22 |
C# Bitmap (1) | 2024.11.18 |
C# 얕은복사, 깊은복사 (1) | 2024.11.14 |
C# 병렬처리1 _ Parallel (0) | 2024.11.11 |