[멀티스레드] Lock구현
lock은 세 가지 방법으로 구현할 수 있다.
- 변수를 점유할 때 까지 쉬지않고 계속 점유를 시도하는 방법
- 점유에 실패하면 일정 시간을 대기하고 점유를 시도하는 방법
- 점유 종료 시점에 발행되는 이벤트를 사용하여 점유를 시도하는 방법
세 가지의 lock을 구현하고 아래의 코드로 테스트 할 것이다.
class Program
{
static Lock _lock = new Lock();
static int number = 0;
static void Thread_1()
{
for(int i = 0; i < 10000; i++)
{
_lock.Enter();
number++;
_lock.Exit();
}
}
static void Thread_2()
{
for (int i = 0; i < 10000; i++)
{
_lock.Enter();
number--;
_lock.Exit();
}
}
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번(SpinLock)
첫 번째 방법으로 구현하는 lock이다.
public class Lock
{
//여러 스레드에서 사용되므로 volatile로 가시성 확보
// 1 = 점유 가능, 0 = 점유 불가능
private volatile int available = 1;
public void Enter()
{
while(true)
{
int expected = 1;
int desired = 0;
if (Interlocked.CompareExchange(ref available, desired, expected) == expected)
break;
/* CompareExchange의 작업
if(available == expected)
{
int origin = available;
available = desired;
return origin;
}
*/
}
}
public void Exit()
{
Interlocked.Exchange(ref available, 1);
}
}
결과를 보면 Lock이 잘 작동하는 것을 확인할 수 있다.
여기서 주의해야 할 곳은 CompareExchange()이다.
if (Interlocked.CompareExchange(ref available, desired, expected) == expected)
break;
점유 가능한 상태인지 확인하고 가능하면 점유하는 부분이다. 이 부분을 일반적으로 구현하면 아래처럼 간단하게 구현 가능하다.
if(available == 1)
available = 0;
Interlocked로 구현한 이유는 원자성이 보장되어야 하는 부분이기 때문이다.
- 점유 가능한지 확인
- 가능하면 점유
두 가지의 작업으로 나누어져 있기 때문에 경합 조건으로 점유 작업이 꼬일 수 있다.
원자성만 보장되면 되기 때문에 꼭 Interlocked로 구현할 필요는 없지만 간단한 작업이기 때문에 Interlocked를 사용하지 않을 이유도 없다.
만약 점유에 실패하면 while(true)이기 때문에 점유에 성공할 때 까지 계속해서 시도하게 된다.
이 방법은 대기 상태가 길어질수록 비효율적인 방법이다. 하지만 유일하게 Context Switcing이 발생하지 않는다는 특징이 있다.
2번
두 번째 방법은 첫 번째 방법에서 잠시 대기하는 작업만 추가하면 된다.
대기하는 방법에도 세 가지가 있다.
- 지정한 밀리세컨드만큼 대기
- 자신보다 우선순위가 같거나 높은 스레드에게 CPU양도
- 모든 스레드에게 CPU양도
public class Lock
{
//여러 스레드에서 사용되므로 volatile로 가시성 확보
// 1 = 점유 가능, 0 = 점유 불가능
private volatile int available = 1;
public void Enter()
{
while(true)
{
int expected = 1;
int desired = 0;
if (Interlocked.CompareExchange(ref available, desired, expected) == expected)
break;
/* CompareExchange의 작업
if(available == expected)
{
int origin = available;
available = desired;
return origin;
}
*/
//Thread.Sleep(10); // 10ms 대기
//Thread.Sleep(0); // 자신보다 우선순위가 같거나 높은 스레드 중 하나에게 양도
Thread.Yield() // 모든 스레드 중 하나에게 양도
}
}
public void Exit()
{
Interlocked.Exchange(ref available, 1);
}
}
대기 상태동안 다른 스레드에게 무조건 CPU를 양도하기 때문에 Context Switching이 발생한다.
3번
3번 방법은 AutoResetEvent라는 것을 사용하여 구현할 수 있다.
AutoResetEvent는 점유 가능한 상태가 되면 이벤트를 발행한다. 스레드는 점유를 시도하였을 때 실패하게 되면 대기 상태에 들어가고 이벤트가 발행되는 시점에 점유를 다시 시도한다.
public class Lock
{
private AutoResetEvent available = new AutoResetEvent(true);
public void Enter()
{
available.WaitOne();
}
public void Exit()
{
available.Set();
}
}
Context Switching이 발생하기 때문에 실행 시간이 크다는 것을 확인할 수 있다.