본문 바로가기
Programing_Language/C#

[쓰레드 동기화]스핀 락(Spin Lock) 구현

by neohtux 2020. 7. 20.
728x90

스핀 락

스핀락은 폴링 방식의 동기화 처리를 하는 방식이다.

마이크로 프로세서 에서 내부 인터럽트를 사용하지 않고 값의 state 가 바뀌는지

while(1) 을 이용하여 확인 하는것과 같다.

 

한 가지 예를 보자

 

num 이라는 데이터를 

첫번째 스레드에선 +1 씩 계속 증가시키고

두번째 스레드에선 -1 씩 계속 감소시키는 행위를 각각 10만번 씩 하면

 

직관적으로는 당연히 프로그램이 뻗지 않는이상 당연히 0이 나올 것이다.

하지만 이전 포스팅에서 자원 접근에 대한 동기화가 이루어 지지 않으면 한 쪽에서 값을 변경 시켜도

다른 쪽에선 이전에 가지고 있던값을 최신이라고 저장하고 있다가 값을 변경시키는 현상이 발생하므로

 

의도했던 데이터 갱신이 이루어지지 않는다.

 

이를 해결하기 위해 첫번째 폴링 방식의 스핀락을 구현할때 발생할 수 있는 문제점과

이를 해결하기 위한 C#에서 제공되는 Interlock의 클래스를 이용한 예를 살펴보자.

 

 

class SpinLock
    {
        volatile int locked = 0;
        public void Acquire()
        {
            while(true)
            {
                if (locked == 1)
                    break; 
            }
         

            locked = 1;
        }
        public void Release()
        {
            locked = 0;
        }
    }

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

        static void Thread_1()
        {
            for(int i=0; i<100000; ++i)
            {
                _lock.Acquire(); // (locked == true);
                num += 1;
                _lock.Release(); // (locked == false) lock 해제
            }
        }
        static void Thread_2()
        {
           for(int i=0; i<100000;++i)
            {
                _lock.Acquire();
                num -= 1;
                _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); //0인지 확인
        }
    }

while(true)
            {
                if (locked == 1)
                    break; 
            }
         

            locked = 1;

 

이 코드를 보면 마치 둘 중에 누군가 먼저 도착하는 스레드가 locked을 잡은다음 값을 1로 바꾸고
다음 스레드가 lcoked이 0인동안 (먼저 온 스레드가 사용중) 대기하고 있다가 locked이 1로 풀리면
사용 하면 될거 같다.

 

하지만, 이 부분에서도 문제가 되는 부분은 저 while 문 내부의 코드이다.

 

어떠한 상황이 발생 할 수 있는지 살펴보면

 

둘이 동시에 locked에 접근하여 0인 state를 갖는것이다. 이렇게 되면

 

첫번째 스레드도, 두번째 스레드도, 

"음 누군가 사용 하고 있군 존버해야겠다."

 

라고 두 스레드 모두 존버하므로 교착상태에 빠지게 된다.

 

이 동시성 제어를 해결하기 위해 

 

C# 에서 Interlocked 클래스에서 제공되는

Exchage를 사용해서 해결한다.

 

물론 이전 포스팅의 Increment도 메모리 베리어를 사용하며 증가 감산의 동시성 제어를 할 수 있다.

 

Interlocked의 Exchange에서 어떻게 작동되는지 코드를 보자.

class SpinLock
    {
        volatile int locked = 0;
        public void Acquire()
        {
            while(true)
            {
                 int original_value = Interlocked.Exchange(ref locked, 1);
                 
                  if (original_value == 0) //만약 원래 값이 0이라면 다른 스레드가 사용하지 않았다고 판단.
                      break;

            }
  


            /*
            while(locked) //폴링 상태로 잠김 대기
             {}
            //lock 획득
             locked = true;*/
        }
        public void Release()
        {
            locked = 0;
        }
    }

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

        static void Thread_1()
        {
            for(int i=0; i<100000; ++i)
            {
                _lock.Acquire(); // (locked == true);
                num += 1;
                _lock.Release(); // (locked == false) lock 해제
            }
        }
        static void Thread_2()
        {
           for(int i=0; i<100000;++i)
            {
                _lock.Acquire();
                num -= 1;
                _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); //0인지 확인
        }
    }

실제 출력을 해보면

교착상태에 빠지지 않고 값이 정상적으로 0이 출력이 된다.

 

while(true)
            {
                 int original_value = Interlocked.Exchange(ref locked, 1);
                 
                  if (original_value == 0) //만약 원래 값이 0이라면 다른 스레드가 사용하지 않았다고 판단.
                      break;
            }

Exchage를 살펴보면

더보기

// 요약:
        //     원자 단위 연산으로 부호 있는 32비트 정수를 지정된 값으로 설정하고 원래 값을 반환합니다.
        //
        // 매개 변수:
        //   location1:
        //     지정된 값으로 설정할 변수입니다.
        //
        //   value:
        //     location1 매개 변수의 설정 값입니다.
        //
        // 반환 값:
        //     location1의 원래 값입니다.
        //
        // 예외:
        //   T:System.ArgumentNullException:
        //     location1의 주소는 null 포인터입니다.

이렇게 요약 되어있는것을 확인할 수 있는데

첫번째 매개변수에는 우리가 자물쇠라고 보통표현하는 잠금장치 값 을 설정해주고 

두번째 매개변수에는 우리가 바꿀 값을 사용해준다.

 

가장 중요한건 반환값인데 여기서 반환값은 우리가 1로 바꾸기 전의 locked에 있는 값이다.

 

따라서 original_value가 0으로 반환 되었다는것은  다른 스레드가 locked을 1로 바꾸기 전,

즉 , 현재 진입한 스레드가 자물쇠를 얻을 수 있다는 말이 된다.

 

그래서 만약, 어떤 스레드가 최초로 저 while 루프를 진입하면

 

"값이 1로 바꾸고 싶어, 그리고 기존 값이 0이면 내가 바꾸고 while루프를 나갈꺼야"

로 해석 된다.

 

그럼 두번째 온 스레드가 첫 진입한 스레드가 작업을 마치지 않았다면 original 값은 1로 바뀌어 있을테고

첫 진입한 스레드가 Release() 함수를 실행하기 전까지 while문에서 무한정 대기할 것이다.

 

 

Interlocked의 Exchange와 비슷한 CompareExchage가 있다.

이름만 봐도 뭔가를 비교해서 바꾸는 함수 같다.

실제 그렇다.

 class SpinLock
    {
        volatile int locked = 0;
        public void Acquire()
        {
            while(true)
            {
                //CAS Compare-AND-Swap
                int expected = 0;
                int desire = 1;
                if(Interlocked.CompareExchange(ref locked, desire, expected) == expected)
                    break;

            }
  
        }
        public void Release()
        {
            locked = 0;
        }
    }

 

CompareExchage를 보면 

첫 번째 파라미터는 자물쇠의 역할이고, 

두 번째 파라미터는 바꾸고자 하는값,

세 번째 파라미터는 locked과 비교하는 값이다.

 

따라서, 만약, locked과 expcted값이 같다면 desire로 값으로 바꾸고

반환 값은 Exchage와 똑같이 원래의 값을 반환한다.

 

좀더 설명을 부연하면,

만약 , locked와 expected값이 0으로 같다 ( 아무도 자물쇠 (locked)를 사용하지 않은 상태이다)

그럼 원래 값 0을 반환시키고 locked을 1로 바꾼다. (다음 순서의 스레드들은 대기하게 된다 1인상태이므로)

 

300x250

댓글