스핀락(SpinLock) 구현하기 1/2
지난 글에서 락Lock과 데드락DeadLock**을 이해하기 위해 화장실 앞에서 기다리는 사람들의 비유를 들었었습니다.
- 문 앞에서 계속 기다린다.
- 별로 안 급하니까 5분 뒤에 다시 오기로 하고 자리로 돌아간다.
- 화장실 옆에 있는 카운터 직원한테 화장실이 비면 내 테이블로 알려달라고 부탁한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class SpinLock
{
volatile bool _locked = false;
public void Acquire()
{
while (_locked)
{
// 잠김이 풀리기를 기다린다.
}
// 내꺼!
_locked = true;
}
public void Release()
{
_locked = false;
}
}
}
지금 봐서는 별 문제 없을 것 같지 않나요? 그럼 이제는 이 스핀락(자물쇠) 클래스를 사용하는 두 쓰레드(두 사람)를 만들어봅시다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
_lock.Acquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 1000000; i++)
{
_lock.Acquire();
_num--;
_lock.Release();
}
}
}
지난번 글에서는 ++, – 연산이 사실 원자적으로 실행되지 않기 때문에 각각 Interlocked.Increment()와 Interlocked.Decrement()로 대체해서 사용했었습니다. 그런데 이번에는 연산 전후에 lock을 걸어놓았으니 굳이 그 사이에 있는 연산이 원자적으로 실행되어야 할 필요는 없어보이죠? 화장실 내에서 무슨 일을 하던 혼자이므로 다른 사람에 의해 간섭받는 일을 없을 듯 합니다. 안심하고 실행해봅시다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using System;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class SpinLock
{
volatile bool _locked = false;
public void Acquire()
{
while (_locked)
{
// 잠김이 풀리기를 기다린다.
}
// 내꺼!
_locked = true;
}
public void Release()
{
_locked = false;
}
}
class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for (int i = 0; i < 1000000; i++)
{
_lock.Acquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 1000000; 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);
}
}
}
뭔가 이상합니다. 똑같이 100만번 더하고 뺐기 때문에 0이 나와야 하는데 이상한 숫자가 나왔습니다. 왜 그랬을까요..? 우리가 만든 Spinlock클래스에 문제가 있는게 틀림없습니다.
우리가 구현해준 Spinlock 클래스의 Acquire() 함수를 보면 동작이 2개로 구분되고 있습니다. 1) 잠금이 풀릴 때까지 밖에서 대기하기…열리면, 2) 들어가서 문잠그기. 잠깐, 동작이 구분되어 있다고 했나요? 원자적atomic**으로 하나처럼 동작하지 않고 **있었군요! 이렇게 원자적으로 동작하지 않고 있다면 멀티쓰레드에서는 서로 다른 쓰레드에 의해서 도중에 간섭을 받을 수 있습니다. 그리고 그 결과는…
두 명 다 들어와버렸습니다. 둘 다 밖에서(while문 안에서) 대기하다가 문이 열리니 동시에 들어와버린 것입니다. 미처 한 사람이 문을 잠그기(_locked = true)도 전에 말이죠. 두명이 들어오다 보니 그 안에서 각자가 하는 일들은 그 원자성이 확보되지 못하게 됩니다.
© sanibell, 출처 Unsplash
화장실에 들어가서 수건함에 수건을 채우고(++) 오는 사람과, 수건을 쓰고(–)오는 사람으로 비유해볼 수도 있을 것 같습니다. 예를 들어서 수건을 채우는 사람(++)이 어떻게 일하는지 봐볼까요? 단, 여기 카페 사장은 결벽증이 있어서 수건을 하나 더 넣으러 갈 때마다 모든 수건을 새 것으로 교체한다고 가정해야 합니다.
| 1 | 화장실에 남은 수건이 몇개 있는지 확인한다. | tmp = number | number라는 메모리 주소에 있는 값을 임시로 가져온다. |
|---|---|---|---|
| 2 | 그 개수보다 한개 더 많도록 가져온 새로운 수건들을 준비한다. | tmp += 1 | 가져온 값에 1을 더한다. |
| 3 | 화장실의 선반에 있는 수건을 모조리 치우고 가져온 새 수건들로만 교체한다. | number = tmp | 다시 number의 메모리 주소에 계산한 결과값을 저장한다. |
만약 수건을 채우는(++) 사람이 1, 2번을 수행하고 3번을 미처 수행하기 전에 수건을 쓰는(–) 사람이 3번(수건을 하나 빼는 일을 온전히 완수했다고 이해하시면 됩니다.)을 먼저 수행했다면 채우는 사람 입장에서는 간섭을 받은 셈이 되어버립니다. 그래서 실제로는 '헌 수건 4개' > '새 수건 5개'로 교체하려 했는데 '헌 수건 3개' > '새 수건 5개'로 교체해버려서 수건 하나를 잘못 세어버린 셈이 되어버린거죠. 이러한 상황이 반복되면 잘못 세었던 수건들의 개수가 점점 늘어나겠죠? 그래서 17970 이라는 이상한 값이 나와버렸고, 반대 상황으로 (-)숫자가 나올 수도 있습니다.
그렇다면 해결방법은 무엇일까요? 다음 포스팅에서 이어서 작성하도록 하겠습니다.
이 글은 인프런의 '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 강의를 참고하여 정리 목적으로 작성하였습니다. 감사합니다.


