포스트

Recursive SpinLock, ReaderWriterLock 구현 연습

  • 재귀적Recursive 락을 허용하거나 안하거나 하는 결정
  • 스핀락 정책. “몇 번 스핀락 루프를 돌고난 후에는 Yield를 한다.”

새롭게 작성하는 C# 클래스 스크립트는 Lock.cs입니다.

지난번 spinLock클래스에는 소유권을 획득하는 Acquire() 메소드, 소유권을 내려놓는 Release() 메소드가 있었습니다. 이번에도 동일한 역할을 하는 writeLock()메소드, unwriteLock()메소드가 있습니다. 다만 여기에서 몇 번 스핀락 루프를 돌았는지 개수를 세어 주게 됩니다. 일단, 재귀적 락은 허용하지 않습니다.

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
// Lock.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    // 재귀적 락을 허용할지 (No)
    // 스핀락 정책 (5000번 -> Yield)
    internal class Lock
    {
        const int EMPTY_FLAG = 0x00000000; // (32비트x)
        const int WRITE_MASK = 0x7FFF0000; // (1비트x), 15비트o, (16비트x)
        const int READ_MASK = 0x0000FFFF; // (16비트x), 16비트o
        const int MAX_SPIN_COUNT = 5000;

        // [Unused(1)] [WriteThreadId(15비트)] [ReadCount(16비트)]
        int _flag = EMPTY_FLAG;

        public void WriteLock()
        {
            // 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 경합해서 소유권을 얻는다
            // 16비트만큼 밀어주니까 총 32비트가 되고, WRITE_MASK를 적용하면, [WriteThreadId(15비트)] 부분만 살아남음.
            int desired = (Thread.CurrentThread.ManagedThreadId 1)가 1로 바꿔놨으니, 다음번에 와서 B(0->1)를 원하는 B는 실행할 수가 없음.
                        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
34
35
36
37
38
39
40
41
42
43
44
45
//Program.cs
using System;
using System.Data;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    internal 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 &#x3D; 0; i < 100000; i++)
                {
                    _lock.WriteLock();
                    count++;
                    _lock.WriteUnlock();
                }
            });

            Task t2 &#x3D; new Task(delegate ()
            {
                for (int i &#x3D; 0; i < 100000; i++)
                {
                    _lock.WriteLock();
                    count--;
                    _lock.WriteUnlock();
                }
            });

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(count);
        }
    }
}

잘 동작합니다. 그럼 재귀적으로 Program.cs만 다음과 같이 두 번 Lock을 해주도록 변경해서 실행해봅시다.

1
2
3
4
5
6
7
8
                for (int i &#x3D; 0; i < 100000; i++)
                {
                    _lock.WriteLock();
                    _lock.WriteLock();
                    count++;
                    _lock.WriteUnlock();
                    _lock.WriteUnlock();
                }

while문에 갇혀버렸군요.

재귀적 락을 허용해봅시다. Lock.cs만 다음과 같이 바꿉니다.

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
//Lock.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    // 재귀적 락을 허용할지 (Yes)
    // WriteLock->WriteLock OK / WriteLock->ReadLock OK,
    // ReadLock->WriteLock No
    // 스핀락 정책 (5000번 -> Yield)
    internal class Lock
    {
        const int EMPTY_FLAG &#x3D; 0x00000000; // (32비트x)
        const int WRITE_MASK &#x3D; 0x7FFF0000; // (1비트x), 15비트o, (16비트x)
        const int READ_MASK &#x3D; 0x0000FFFF; // (16비트x), 16비트o
        const int MAX_SPIN_COUNT &#x3D; 5000;

        // [Unused(1)] [WriteThreadId(15비트)] [ReadCount(16비트)]
        int _flag &#x3D; EMPTY_FLAG;
        int _writeCount &#x3D; 0; //재귀적으로 몇개의 락을 할지를 관리.

        public void WriteLock()
        {
            // 특수한 경우. 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
            int lockThreadId &#x3D; (_flag & WRITE_MASK) >> 16; // 0x00007FFF
            if (Thread.CurrentThread.ManagedThreadId &#x3D;&#x3D; lockThreadId)
            {
                _writeCount++;
                return;
            }

            // 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 경합해서 소유권을 얻는다
            // 16비트만큼 밀어주니까 총 32비트가 되고, WRITE_MASK를 적용하면, [WriteThreadId(15비트)] 부분만 살아남음.
            int desired &#x3D; (Thread.CurrentThread.ManagedThreadId > 16; // 0x00007FFF
            if (Thread.CurrentThread.ManagedThreadId &#x3D;&#x3D; lockThreadId)
            {
                Interlocked.Increment(ref _flag);
                return;
            }

            // 아무도 WriteLock을 획득하고 있지 않으면, ReadCount를 1 늘린다.
            while (true)
            {
                for (int i &#x3D; 0; i 1)가 1로 바꿔놨으니, 다음번에 와서 B(0->1)를 원하는 B는 실행할 수가 없음.
                        return;
                }
                Thread.Yield();
            }
        }  

        public void ReadUnlock()
        {
            Interlocked.Decrement(ref _flag);
        }
    }
}

재귀적으로 잘 빠져나왔습니다. 그렇다면 마지막으로, 만약 lock과 unlock의 짝이 맞지 않는다면?

1
2
3
4
5
6
7
                for (int i &#x3D; 0; i < 100000; i++)
                {
                    _lock.WriteLock();
                    _lock.WriteLock();
                    count++;
                    _lock.WriteUnlock();
                }

짝이 안 맞으면 못 빠져나오게 됩니다. 락을 안 푸니까 풀릴 때까지 기다리기 때문입니다.


이 글은 인프런의 '[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 강의를 참고하여 정리 목적으로 작성하였습니다. 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.