개인공부) 서버실습(11) - SpinLock

Justin·2022년 6월 2일
0

서버공부

목록 보기
10/45

✅ 지난 시간

어제는 SpinLock, Context Switching, Auto Reset Event 라는 Lock의 세가지 동작 상태에 대해 배웠고, 오늘은 그 중에서 SpinLock에 대해 조금 더 깊게 배울 것이다.

💈 SpinLock

이미 차지되어있는 Lock에 접근하기 위해 계속 대기중인 상태

💢 문제가 있는 SpinLock

namespace ServerCore
{
    public class SpinLock
    {
        volatile public bool _lock = false;
        public void Acquire()
        {
            while(_lock)
            {

            }
            _lock = true;
        }
        public void Release()
        {
            _lock = false;
        }
    }

    public  class Program
    {
        static SpinLock spin = new SpinLock();
        static int num = 0;

        static void Thread_1()
        {
            for (int i = 0; i < 10000; i++)
            {
                spin.Acquire();
                num++;
                spin.Release();
            }
        }
        static void Thread_2()
        {
            for (int i = 0; i < 10000; i++)
            {
                spin.Acquire();
                num--;
                spin.Release();
            }
        }
    }
}
// 메인 함수에서는 Task를 통해 동작

위와 같이 Acquire()을 통해 내가 lock으로 들어간 뒤 잠궈주고, 나올 때 Release()로 풀어주는 구조로 되어있을 경우 과연 문제가 발생할까?

언뜻 보기에는 괜찮게 잘 돌아갈 것 처럼 느껴지나 실제로 돌려보면 알 수 없는 값이 나온다.

왜 이럴까?

🟠 문제 포인트

 public void Acquire()
        {
            while(_lock)
            {

            }
            _lock = true;
        }

이렇게 문을 잠구고자 할 때 싱글 쓰레드라면 문제가 없을 수 있으나, 멀티 쓰레드의 경우는 우연히도 while문 안에 다른 쓰레드들도 접근하는 경우가 생길 수 있다.

그렇게 되면 잠구어도 같이 들어온 상태이기에 num 처리가 예상했던 대로 이루어지지 않는 것이다.

🟢 Interlocked.Exchange 를 이용한 해결법

문제파악
while을 안에 들어와 처리를 하고 문을 잠구는 행위가 나뉘어지기에 다른 멀티 쓰레드들이 접근하며 예상하지 못하는 값이 출력된다.

지난 번 num++ 이 세 단계로 구분되어 같이 Interlocked.Increment 를 통해 한 단계로 압축하여 풀었던 거 같이. 이번에도 원자성이 보존될 필요가 있다.

문제 해결 방법은 지난 번과 유사하여 크게 다르지 않았다. 일단 Interlocked을 사용할 것이니 bool을 통해 잠금이 아닌 int를 통한 잠금 구조로 변경한다.

계속 대기를 하며 처리를 하고, 0이 되면 탈출하는 구조이다. 여기서 Interlocked.Exchange는 ref 키워드를 통해_lock 값을 1로 변경해주게 된다.


Interlocked로 다른 쓰레드가 접근하지 못한 상태로 처음 들어와서 1로 변경해주며 해당 lock을 차지하게 되고, 사용후 0으로 변환해준다.

여기서 핵심 포인트는 origin 변수이다. 변경 되기 전에 값을 return 해주어 원래 값 체크를 통해 해당 lock이 비어있었는지 아닌지를 체크해주게 된다.

public void Acquire()
        {
            while(true)
            {
                int origin =  Interlocked.Exchange(ref _lock, 1);
                if (origin == 0)
                    break;
            }
        }

Interlocked.Exchange가 한 일을 더 자세하게 보자면 아래의 과정을 interlocked가 묶어준거라고 보면 된다.

	int origin = _lock
	_lock = 1
    
    if(_lock == 0)
    	break;

🧾 다시 정리하자면


낮은 확률로 while 안에 여러개의 쓰레드가 동시에 들어올 경우라도, A 같이 먼저 들어간 쓰레드가 origin 0 을 차지하고 있기에 A가 나가기 전 까지 다른 쓰레드들은 while 문 안에서 대기해야한다.

🟢🟢 더 쉬운 Interlocked.CompareExchange

Interlocked.CompareExchange(ref 변경 대상, 변경 하고자하는 값, 비교 대상)을 대입하여 사용
c++에서는 2번째 인자를 expected, 세번째를 desired라 표기하여 헷갈리지 않게 한다.

오늘 진행했던 예시에서는 Exchange를 해주어도 큰 문제가 없지만 조금 더 범용적으로 사용되는 Interlocked.CompareExchange 도 알아주어야한다.

아래와 같이 값을 대입하는 방식이 Interlocked.Exchange 라면

	int origin = _lock
	_lock = 1
    
    if(_lock == 0)
    	break;

Interlocked.CompareExchange는 이런 식으로 값을 무조건 대입하기 보다는 비교해서 넣어주는 기능을 한다.

    if(_lock == 0)
    	_lock = 1;

CAS(Compare-And-Swap)
비교후 변경해주는 계열의 함수를 CAS라고 부른다


사용법은 같으나, 비교 후에 대입한다라는 점에서 더 안전성 있게 사용이 가능하다.


📀 Saving..

Spinlock은 멀티쓰레드 관련 기본 지식으로 꼭 알아두어야 한다. 확실히 코드를 잘 못 짤경우 spine만 계속하면서 Deadlock이 될 수도 있을 거 같다.

SpinLock은 대기 시간이 길지 않은, 짧은 시간 내에 접근 가능한 구조일 때 사용해주면 될듯 싶다.

profile
인디 게임을 만들며 공부하고 있습니다.

0개의 댓글