1. SpinLock
개념
잠금을 획득하려는 스레드가 잠금을 사용할 수 있을 때까지 반복적으로 확인하는 루프에서 기다리는 상호 배제 잠금 기본 요소를 제공
공유자원의 점유가 가능한지 반복적으로 확인하는 방식으로 공유자원 점유를 위한 대기시간이 길어질수록 효율이 떨어짐
코드
class SpinLock {
// 잠금의 상태(0 -> 점유가능, 1 -> 점유 불가능)
int locked = 0;
public void Acquire() {
// 반복적으로 공유자원의 lock 해제되었는지 확인
while (true)
{
// locked -> 잠금상태(1)로 변경
// return 값은 변경하기 전의 값으로 다른 스레드에 의해서 값이 변경되지 않는 오리지널 값을 뜻함
int original = Interlocked.Exchange(ref locked, 1);
// 잠금되지 않은 경우에만 잠금처리를 한다(이미 잠금상태인 경우라면 잠금처리 안함)
if (original == 0) {
break;
}
// 위의 코드를 좀 더 직관적으로 바꿔보자면 아래와 같이 변경 가능하다.
// 첫번째 인자와 세번째 인자를 비교해 값이 동일하다면 두번째 인자로 변경한다
if (Interlocked.CompareExchange(ref locked, 1, 0) == 0) {
break;
}
}
}
public void Release()
{
this.locked = 0;
}
}
class Program
{
static int number = 0;
static SpinLock spinLock = new SpinLock();
static void Thread_1() {
for (int i = 0; i < 100000; i++)
{
spinLock.Acquire();
number++;
spinLock.Release();
}
}
static void Thread_2() {
for (int i = 0; i < 100000; i++)
{
spinLock.Acquire();
number--;
spinLock.Release();
}
}
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);
}
}
2. 신호에 의한 쓰레드 lock
개념
ThreadB가 공유자원을 미리 선점하고 있고 ThreadA가 공유자원을 점유하기 위해서 접근을 했을 때
공유자원에 접근 할 수 없는 상태라면 신호(공유자원에 접근 할 수 있는지 없는지)를 만들어서
ThreadA에게 공유자원에 대한 접근 정보를 신호를 통해 알려준다.
이 신호에 대한 것은 운영체제의 커널단에서 발생되기 때문에 속도가 SpinLock 비해서 느리다.
코드
대표적으로 AutoResetEvent와 ManualResetEvent 2가지 클래스가 있음
AutoResetEvent의 경우 차량이 돈을 내고 차단기를 지나가면 차단기가 자동으로 닫히지만,
ManualResetEvent은 차단기가 자동으로 닫히지 않기 때문에 수동으로 닫아야 한다.
함수 설명
1. 클래스의 매개인자(bool) : false인 경우 모든 스레드는 신호를 기다리고 true로 설정한 경우 어떤 스레드도 신호를 기다리지 않음
2. WaitOne : 신호를 받을 때까지 공유자원에 접근하는 현재 스레드를 차단
(미리 대기하지 말고 신호가 있으면 공유자원으로 접근하라는 뜻임)
Auto의 경우 공유자원에 접근해도 좋다는 신호가 발생하면 이벤트 상태를 신호 없음 상태로 자동설정
3. Set : 공유자원을 점유하고 있는 스레드에 대한 lock을 풀고 대기상태에 있는 스레드가 공유자원에 접근 할 수 있게 함
(이벤트 상태를 신호 수신 상태로 설정)
4. Reset : 이벤트의 상태를 신호 없음 상태로 설정
(공유자원에 접근해도 좋다는 신호를 받았으면 그 이후에는 신호가 필요 없어지는 경우도 있기 때문에)
namespace ServerCore {
class AutoLock {
AutoResetEvent resetEvent = new AutoResetEvent(true);
public void Acquire()
{
resetEvent.WaitOne();
}
public void Release()
{
resetEvent.Set();
}
}
class ManualLock {
ManualResetEvent resetEvent = new ManualResetEvent(false);
public void Acquire()
{
resetEvent.WaitOne();
resetEvent.Reset();
}
public void Release()
{
resetEvent.Set();
}
}
class Program
{
static int number = 0;
static Lock _lock = new Lock();
static void Thread_1() {
for (int i = 0; i < 1000000; i++)
{
_lock.Acquire();
number++;
_lock.Release();
}
}
static void Thread_2() {
for (int i = 0; i < 1000000; i++)
{
_lock.Acquire();
number--;
_lock.Release();
}
}
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);
}
}
}
ManualResetEvent의 경우 원자성으로 코드가 실행되지 않기 때문에 동기화가 되지 않는 문제가 발생 할 수 있음
A코드를 실행하고 나면 B코드가 바로 실행되는게 아니라 A코드의 실행이 끝나고 다른 스레드의 코드가 실행된 다음에 B코드가 실행 될 수 있음
참고 : https://medium.com/@akshay.babannavar/manualresetevent-and-autoresetevent-in-c-2d0dc8724ccd