class SpinLock
{
volatile bool _locked = false; // false: 아무도 사용하고 있지 않다.
public void Acquire() // 획득하겠다.
{
// 락이 풀릴때까지 기다려야 한다.
while (_locked)
{
}
_locked = true;
}
public void Release() // 획득한것을 내려놓겠다.
{
_locked = false;
}
}
이렇게 할 수 있다. (하지만 문제가 있다.)
나머지 코드는 아래와 같다.
internal class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for(int i = 0; i < 100000; i++)
{
_lock.Acquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 100000; i++)
{
_lock.Acquire();
_num--;
_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(_num);
}
}
위 코드는 문제가 있다. 최종 _num값이 0이 나오지 않는다. 그 이유른 두 스레드가 또 거의 동시에 실행될때 발생하는 것이다.
while (_locked)
{
}
_locked = true;
이부분이 문제 인 것이다. 두 스레드가 거의 동시에 실행되고 있어 _locked를 아무도 true로 바꾸지 않고 while문을 넘어가면 문제가 되는 것이다. 스레드는 누가 같이 _locked키를 얻었는지 관심이 없다. 그저 키를 획득했으면 자신의 일을 할 뿐이다.
원자성과 관련이 있다.
while (_locked)
{
}
_locked = true;
이 부분은 원자적으로 실행되어야한다.
Interlocked계열의 함수들을 이용해보자.
// _locked를 int로 바꿔야지 아래 코드가 동작한다.
public void Acquire()
{
while (true)
{
int original = Interlocked.Exchange(ref _locked, 1);
if (original == 0)
break;
}
}
이렇게 하면 _num의 최종값이 0이 된다. Interlocked.Exchange이 함수는 단지 _locked의 값을 1로 바꾸는 것이다. 반환값이 중요한데 반환값의 의미는 내가 바꾸기 이전 값을 의미한다. 0이 반환되면 아무도 이 키를 가지고 있지 않았다는 의미고 1이 반환되면 누군가 이 키를 가지고 있다는 의미이다.
이렇게 하면 문제는 해결되지만 직관적이지 않고 경우에 따라 위험할 수 있다.
Interlocked.Exchange의 의미는
int original = _locked;
_locked = 1;
이런 의미인데 사실 이것보다는
if(_locked == 0)
_locked = 1;
이것이 더 직관적이다.
그리고 경우에 따라 위험한 이유는 무작적 1로 대입한다는 것이다.
Interlocked의 다른 함수가 하나 더 있다.
while (true)
{
// 첫 인자와 세번째 인자를 비교한다. 같으면 두번째 인자로 바꾼다.
int original = Interlocked.CompareExchange(ref _locked, 1, 0);
if (original == 0)
break;
}
이런 방식을 이런 방식을 CAS(Compare-And-Swap)방식이라고 한다. 비교 후 변경해 준다는 것을 의미한다.
코드를 좀 더 정리하면 이렇게 쓸 수 있는데 이렇게 쓰는 것이 좀 더 직관이기도 하다.
int expected = 0; // 예상되는
int desired = 1; // 원하는
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
break;