ReaderWriterLock을 어떤식으로 동작하는지 구현해본다.
(주석 확인)
먼저 Lock 클래스를 생성하고 다음과 같이 변수를 선언한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Lock
{
const int EMPTY_FLAG = 0x00000000;
const int WRITE_MASK = 0x7FFF0000; // 쓰기
const int READ_MASK = 0x0000FFFF; // 읽기
const int MAX_SPIN_COUNT = 5000; // Spin 최대 횟수
// [Unused(1)] [WriteThreadId(15)] [ReadCount(16)]
int _flag;
// 쓰레드가 몇번의 WriteLock을 흭득하는지 확인
int _writeCount = 0;
}
_flag는 32비트 안에서 각각 Unused(1), WriteThreadId(15), ReadCount(16)로 사용될 것이다.
WriteLock은 어떠한 스레드도 WriteLock과 ReadLock을 흭득하지 않았을 때 경합해서 소유권을 얻는다.
WriteLock은 현재 들어온 스레드의 ID를 가져와서 다른 스레드가 사용중이 아니라면 _flag에 넣는다.
또한 현재 사용중인 스레드가 또 사용한다면 _writeCount의 횟수를 증가시킨다.
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
public void WriteLock()
{
// 동일 쓰레드가 WriteLock을 이미 흭득하고 있는지 확인
int lockThreadID = (_flag & WRITE_MASK) >> 16;
if (lockThreadID == Thread.CurrentThread.ManagedThreadId)
{
_writeCount++;
return;
}
// desired : 스레드 id를 16진수로 변환 -> WRITE_MASK와 같은 위치에 값을 반환
int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
while (true)
{
// 최대 Spin 횟수만큼 돌기
for(int i=0; i<MAX_SPIN_COUNT; i++)
{
// 다른 스레드가 사용중이 아니라면 _flag에 desired(ThreadID) 넣기
if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
{
_writeCount = 1;
return;
}
}
// 안되면 잠시 기다렸다 오기.
Thread.Yield();
}
}
public void WriteUnLock()
{
// 현재 사용되는 스레드의 WriteLock 사용 끝내기 (횟수 감소)
int lockCount = --_writeCount;
if (lockCount == 0)
Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}
ReadLock 구현도 WriteLock과 비슷하다.
ReadLock은 독점권이 아닌 다른 스레드들끼리 동시에 사용 가능하기 때문에
_flag의 READ_MASK 자리를 가져와서 횟수만 +1 증가시킨다.
또한 WriteLock을 사용하는 스레드가 동시에 ReadLock을 사용할 수 있기 때문에
쓰레드 ID도 확인해주도록 한다.
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
public void ReadLock()
{
while (true)
{
// 동일 쓰레드가 WriteLock을 이미 흭득하고 있는지 확인
int lockThreadID = (_flag & WRITE_MASK) >> 16;
if (lockThreadID == Thread.CurrentThread.ManagedThreadId)
{
Interlocked.Increment(ref _flag);
return;
}
for (int i = 0; i < MAX_SPIN_COUNT; i++)
{
// _flag에 READ_MASK 자리 추출 후 사용.
int expected = (_flag & READ_MASK);
if (Interlocked.CompareExchange(ref _flag, expected+1, expected) == expected)
return;
}
Thread.Yield();
}
}
public void ReadUnLock()
{
Interlocked.Decrement(ref _flag);
}
사용하기
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
static volatile int _count = 0;
static Lock _lock = new Lock();
static void Main(string[] args)
{
// 메소드 구현이 귀찮으니 Delegate로 구현
Task t1 = new Task(delegate ()
{
for(int i=0; i<1000000; i++)
{
_lock.WriteLock();
_count++;
_lock.WriteUnLock();
}
});
Task t2 = new Task(delegate ()
{
for (int i = 0; i < 1000000; i++)
{
_lock.WriteLock();
_count--;
_lock.WriteUnLock();
}
});
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(_count);
}
이렇게 ReaderWriterLock을 구현함으로써 구체적인 코드 구성을 이해할 수 있었다.
특히 if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
는
이해가 어려울 수 있으므로 더욱 분석하여 공부하도록 한다.
전체 코드
[ Source Code (Click) ]
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
class Lock
{
const int EMPTY_FLAG = 0x00000000;
const int WRITE_MASK = 0x7FFF0000;
const int READ_MASK = 0x0000FFFF;
const int MAX_SPIN_COUNT = 5000;
// [Unused(1)] [WriteThreadId(15)] [ReadCount(16)]
int _flag;
int _writeCount = 0;
// 아무도 WriteLock or ReadLock을 흭득하지 않고 있을 때, 경합해서 소유권을 얻는다.
public void WriteLock()
{
// 동일 쓰레드가 WriteLock을 이미 흭득하고 있는지 확인
int lockThreadID = (_flag & WRITE_MASK) >> 16;
if (lockThreadID == Thread.CurrentThread.ManagedThreadId)
{
_writeCount++;
return;
}
// desired : 스레드 id를 16진수로 변환 -> WRITE_MASK와 같은 위치에 값을 반환
int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
while (true)
{
for(int i=0; i<MAX_SPIN_COUNT; i++)
{
if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
{
_writeCount = 1;
return;
}
}
Thread.Yield();
}
}
public void WriteUnLock()
{
int lockCount = --_writeCount;
if (lockCount == 0)
Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}
public void ReadLock()
{
int lockThreadID = (_flag & WRITE_MASK) >> 16;
if (lockThreadID == Thread.CurrentThread.ManagedThreadId)
{
Interlocked.Increment(ref _flag);
return;
}
while (true)
{
for (int i = 0; i < MAX_SPIN_COUNT; i++)
{
int expected = (_flag & READ_MASK);
if (Interlocked.CompareExchange(ref _flag, expected+1, expected) == expected)
return;
}
Thread.Yield();
}
}
public void ReadUnLock()
{
Interlocked.Decrement(ref _flag);
}
}
class Program
{
static volatile int _count = 0;
static Lock _lock = new Lock();
static void Main(string[] args)
{
Task t1 = new Task(delegate ()
{
for(int i=0; i<1000000; i++)
{
_lock.WriteLock();
_count++;
_lock.WriteUnLock();
}
});
Task t2 = new Task(delegate ()
{
for (int i = 0; i < 1000000; i++)
{
_lock.WriteLock();
_count--;
_lock.WriteUnLock();
}
});
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(_count);
}
}