C# 멀티스레딩에서 안전한 공유 자원 관리를 위한 임계영역과 Lock 사용법
예제 코드 소개
아래의 예제 코드는 두 개의 스레드가 number 변수를 각각 증가 및 감소시키는 방식으로 동작합니다. 그러나 동시 접근으로 인해 number 값이 예상과 다르게 나올 수 있습니다.
class Program
{
static int number = 0;
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
{
number++;
}
}
static void Thread_2()
{
for (int i = 0; i < 10000; i++)
{
number--;
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
1. 임계영역 (Critical Section)
임계영역은 동시에 접근해서는 안 되는 공유 자원에 접근하는 코드 영역입니다. 임계영역은 한 번에 하나의 스레드만 접근할 수 있도록 해야 합니다. 위 예제에서 number 변수에 대한 접근이 임계영역에 해당합니다.
2. 상호배제 (Mutual Exclusion)
상호배제는 두 개 이상의 스레드가 동시에 임계영역에 들어가지 못하게 하는 기법입니다. 이는 공유 자원의 일관성을 보장하는 데 필수적입니다. 상호배제를 구현하기 위해 C#에서는 lock 구문을 사용할 수 있습니다.
3. Lock
lock 구문은 특정 코드 블록에 대해 한 번에 하나의 스레드만 접근하도록 합니다. lock은 객체를 기반으로 하여 동작하며, 해당 객체가 다른 스레드에 의해 잠겨 있는 동안에는 다른 스레드가 해당 객체에 접근할 수 없습니다.
class Program
{
static int number = 0;
static object _obj = new object();
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
{
lock (_obj)
{
number++;
}
}
}
static void Thread_2()
{
for (int i = 0; i < 10000; i++)
{
lock (_obj)
{
number--;
}
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
이제 number 변수에 대한 접근이 안전하게 보호되며, 최종 출력은 항상 0이 됩니다.
4. 데드락 (Deadlock)
데드락은 두 개 이상의 스레드가 서로 상대방이 소유한 자원을 기다리면서 무한 대기 상태에 빠지는 현상입니다. lock을 잘못 사용하면 데드락이 발생할 수 있습니다. 데드락을 방지하려면 자원 접근 순서를 일관되게 유지하고, 중복 잠금을 피해야 합니다.
5. try{}finally{} 블록
lock 구문은 내부적으로 try{}finally{} 블록을 사용하여 예외가 발생하더라도 락을 해제합니다. 이를 직접 사용할 수 있습니다:
class Program
{
static int number = 0;
static object _obj = new object();
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
{
try
{
Monitor.Enter(_obj);
number++;
return;
}
finally
{
Monitor.Exit(_obj);
}
}
}
static void Thread_2()
{
for (int i = 0; i < 10000; i++)
{
try
{
Monitor.Enter(_obj);
number--;
return;
}
finally
{
Monitor.Exit(_obj);
}
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
6. Monitor
Monitor 클래스는 lock 구문의 내부적으로 사용하는 클래스입니다. Monitor.Enter는 특정 객체를 잠그고, Monitor.Exit는 잠금을 해제합니다. lock 구문과 달리, Monitor는 잠금 해제 여부를 더 세밀하게 제어할 수 있습니다.
Monitor 사용 예시
위 예제에서 lock 구문을 Monitor로 대체한 것입니다. Monitor.Enter와 Monitor.Exit을 사용하여 명시적으로 잠금을 관리할 수 있습니다.
결론
이번 포스트에서는 C#에서 멀티스레딩 프로그래밍 시 발생할 수 있는 경합 조건 문제와 이를 해결하기 위한 임계영역, 상호배제, lock 구문, 그리고 Monitor 클래스에 대해 알아보았습니다.
멀티스레드 프로그래밍에서 안전하게 공유 자원에 접근하려면 다음 사항들을 고려해야 합니다:
- 임계영역을 설정하여 동시 접근을 제한합니다.
- 상호배제를 통해 한 번에 하나의 스레드만 접근하도록 합니다.
- lock 구문이나 Monitor 클래스를 사용하여 안전한 동기화를 구현합니다.
- 데드락을 피하기 위해 자원 접근 순서를 일관되게 유지합니다.
이러한 개념을 적용하면 멀티스레딩 환경에서도 안전하고 일관된 프로그램을 작성할 수 있습니다. 추가적인 질문이나 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!
'Programming > C#' 카테고리의 다른 글
C# SpinLock 효율적인 스레드 동기화 기법 (0) | 2024.07.01 |
---|---|
C# 멀티스레딩에서 데드락 문제와 해결 방법 (0) | 2024.07.01 |
C# 멀티스레딩에서 경합 조건과 Interlocked 클래스의 사용 (0) | 2024.07.01 |
멀티스레딩에서 발생하는 메모리 재정렬 현상과 해결 방법 (0) | 2024.07.01 |
C# 다차원 배열 순회 행 우선 순회와 열 우선 순회의 성능 차이 (0) | 2024.07.01 |