Interlocked / Lock 기초

개발조하·2024년 1월 20일

GameServer

목록 보기
7/9
post-thumbnail

1. 공유 변수 접근에 대한 문제점

2. 경합 조건(Race condition)

  • Race condition이란?
    두 개 이상의 프로세스나 스레드가 공유된 데이터에 동시에 접근하려고 할 때, 그 순서에 따라 실행 결과가 달라질 수 있는 상황을 말한다.
    즉, 공유된 자원에 대한 접근 순서가 결과에 영향을 미치는 상황.

2.1 문제원인

주요 문제는 number++와 number-- 연산이 '원자적(atomic)이지 않다'는 것.
원자적 연산은 중간 단계 없이 한 단계로 완전히 수행되는 연산을 의미한다.
number++나 number-- 연산은 세 부분으로 나뉩니다:

number++;의 연산 과정

  1. 메모리로부터 값 읽기
    : CPU는 메모리 주소에서 현재 값을 읽어 레지스터로 가져온다. 이 단계에서 메모리에서 현재 number의 값을 CPU로 로드한다.

  2. 연산 수행
    : CPU 내부에서 레지스터의 값을 증가시키거나 감소시킨다. 이 경우 number의 값이 1 증가하거나 감소한다.

  3. 메모리에 값 쓰기
    : 연산된 결과를 다시 메모리 주소에 저장한다. 변경된 number의 값이 메모리에 저장된다.

2.2 문제 해결 (Interlocked)

CPU명령에서 '원자적으로 연산'한다.
ㄴ 결과값: 0

(ref number)로 number변수의 주소값을 참조(ref)하여 해당 주소값에 있는 number의 값을 1씩 증가/감소시키기 때문에 '원자적'연산이 가능하다.
ㄴ 해당 값을 읽고 넘겨서 더하고 다시 넘겨주는 과정이 없기 때문!

namespace ServerCore
{
    internal class Program
    {
        static int number = 0;
        static void Thread_1()
        {
            for (int i = 0; i < 1000000; i++)
            {
                Interlocked.Increment(ref number);
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 1000000; i++)
            {
                Interlocked.Decrement(ref number);
            }
        }
        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);
        }
    }
}

Interlocked의 경우 이미 메모리 배리어가 일어나고 있기 때문에 가시성을 위해 변수에 volatile을 굳이 작성하지 않아도 된다.

Thread_1()과 Thread_2()가 경합하여 만약 Thread_1()이 먼저 시작했다면, Interlocked.Increment()가 다 실행된 후에야 Interlocked.Decrerment()가 실행될 수 있다.
즉, 순서를 보장받는다.

❌ 하지만, Race condition을 Interlocked 코드로 해결하는 것의 가장 치명적인 문제는 'int 정수'만 사용할 수 있다는 것이다..!

3 Lock 걸기

3.1 상호배제 (Mutual Exclusive)

즉, 나만 사용할 것이다. 다른 사람들은 얼씬도 하지말라는 뜻.

  • Monitor.Enter(): 문을 잠그는 행위
  • Monitor.Exit(): 잠금을 풀어준다.
    ㄴ 이렇게 진행하면 number++;이 진행되는 동안 number--;가 실행되지 못하기 때문에 race condition의 문제가 해결된다.

3.2 데드락(Dead Lock)

현재처럼 Monitor.Enter()와 Exit() 사이에 실행할 코드가 간결하면 괜찮은데, 만약 코드가 길어져서 실수로 안에 return;을 작성해버리면 Exit()로 잠금을 풀어주지 못했기 때문에 코드가 마무리되지 못하고 그 안에서 갇혀버리는 현상이 발생한다.
이것이 데드락이다.

💡 데드락이 되지 않도록 하려면?

  • try~finally문 사용
    ㄴ 하지만 번거롭다...

3.3 lock() 사용!

namespace ServerCore
{
    internal class Program
    {
        static int number = 0;
        static object _obj = new object();

        static void Thread_1()
        {
            for (int i = 0; i < 1000000; i++)
            {
                lock (_obj)
                {
                    number ++;
                }
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 1000000; i++)
            {
                lock (_obj)
                {
                    number--;
                }
            }
        }

        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);
        }
    }
}

📄참고자료
[인프런] c#과 유니티로 만드는 MMORPG 게임 개발 시리즈_4. 게임 서버

profile
Unity 개발자 취준생의 개발로그, Slow and steady wins the race !

0개의 댓글