Critical Section을 구현하는 두 번째 방법에 대해 알아서 보자.

Spinlock처럼 화장실 문이 열릴 때 까지 앞에서 계속 기다리는 방법도 있지만

위의 그림 처럼 잠깐 쉬다가 다시 돌아오는 방법도 있다. 단, 이 방법은 쉬다가 다시 돌아올 때 화장실 문이 열려 있다는 보장이 없다.

이 방법은 정말 간단하게 코드로 구현할 수 있다.


public void Acquire()
{
	while (true)
	{
		int expected = 0;
		int desired = 1;

		//Compare and Swap(CAS)
		if(expected == Interlocked.CompareExchange(ref _locked, desired, expected))
		{
			break;
		}
        	// Release 시, _locked에 0을 대입
		// _locked 가 comparand(expected)와 같다면 _locked에 desired을 대입
		//  Interlocked.CompareExchange의 반환값은 원래 _locked이 가지고 있던 값. 


		// 쉬다 올께 --> 셋 중 하나 선택 
		Thread.Sleep(1); // 무조건 휴식 -> 1ms 정도 쉬고 싶은데 정확한 시간은 운영체제가 결정
		Thread.Sleep(0); // 조건부 양보 -> 나보다 우선순위가 낮은 애들한테는 양보 안함 
		// 우선순위가 높거나 동일한 쓰레드가 없으면 다시 본인한테 

		Thread.Yield(); // 관대한 양보 -> 관대하게 양보할테니, 지금 실행가능한 쓰레드가 있으면 양보할께요.
		// 없으면 자기가 함. 
	}

}

Spinlock을 구현할 때 쓴 코드를 조금 수정했다.

while문을 무작정 반복하는게 아니라 쉬거나 다른 쓰레드들에게 양보를 해주는 방법이다.

허나 이와 같은 양보가 무작정 좋지는 않다. Context Switching 비용을 생각해야 하기 때문이다.


Context Switching은 쓰레드끼리 교환이 일어날 때,
기존 쓰레드의 정보를 저장하고 깨울 쓰레드의 정보를 복원하는 일련의 과정을 말하며, Lock을 쓰지 않더라도 늘상 발생하고 있는 자연스러운 현상이다.


영화나 드라마에서 귀신한테 빙의를 당할 때 보면, 빙의 시에는 이상 행동을 하다가 빙의가 끝나면 다시 원래대로 돌아온다.


운영체제도 이와 유사한 형태의 동작을 한다.

관리자(커널모드)가 직원(쓰레드)한테 빙의할 때는 컨텍스트 안에 모든 정보를 복원해주어야 한다.

즉, 메모리에서 프로그램과 관련된 정보를 뽑아와야 하고 그 일부를 레지스터에 저장해야 한다.

이 중 지리적인 정보는 가상메모리와 관련이 있다. 프로그램이 같으면 가상 메모리를 안 바꿔도 되는데 프로그램이 다르면 가상 메모리를 바꿔서 프로그램에 의한 정보도 새로 추출해야 한다.


아무튼 이런 식으로, 빙의 시 마다 레지스터에 있는 정보가 날라가고 새로운 정보가 저장된다. 그 날라간 정보는 메모리에 저장해서 나중에 꺼내쓸 수 다.

따라서 우리가 쓰레드를 바꿀 때는 관리자모드(커널모드)로 옮겨간 다음 온갖 정보를 복원하고 빙의하는 것이다.


그렇기에 다른 쓰레드에게 양보하는 행위가 무작정 좋은 행동이 아닌 것이다.


다음은 Critical Section을 구현하는 마지막 3번째 방법을 알아보자.

직원한테 요청을 해서 화장실이 비었을 때를 알려달라는 방법이다.

하지만 직원이 커널레벨 관리자이므로 직원과의 소통에서 Context Switching이 발생하므로 작업이 느리다는 단점이 있다.


이를 AutoResetEvent로 구현할 수 있다.

    class Lock
    {
        AutoResetEvent _available = new AutoResetEvent(true); // initial state가 true -> 문이 열린 상태 , false -> 문이 닫힌 상태 

        public void Acquire()
        {
            _available.WaitOne(); // 문이 열리면 입장이 가능하다.
                                  // autoResetEvent는 입장이 완료되면 자동으로 문이 닫힌다.
            
        }

        public void Release()
        {
            _available.Set(); // 이벤트의 상태를 시그널로 바꾼다. -> initial state를 true로 바꾼다. 
        }
    }

    class Program
    {
            

        static int number = 0;
        static Lock _lock = new Lock(); 

        static void Thread_1()
        {
            for(int i=0; i< 10000; i++)
            {
                _lock.Acquire();

                number++; // 
                //Console.WriteLine($"Plus Number {i}");

                _lock.Release();
            }
        }

        static void Thread_2()
        {

            for (int i = 0; i < 10000; i++)
            {
                _lock.Acquire();

                number--;
                //Console.WriteLine($"Minus Number {i}");

                _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(number);

        }
    }

Lock 클래스를 제외하고는 SpinLock을 구현할 때랑 똑같은 코드이다.

AutoResetEvent는 인스턴스를 만들면서 Initial State(초기 상태)를 정의할 수 있다.

new AutoResetEvent(true) 라고 하면 화장실의 문이 열린 상태이고, new AutoResetEvent(false) 라고 하면 화장실의 문이 닫힌 상황이다.


AutoResetEvent.WaitOne는 문이 열릴 때 관리자가 알려줘서 입장을 가능하게 해준다. 여기서 AutoReset은 자동으로 문을 닫아준다는 뜻인데, WaitOne으로 입장하면 자동을 문을 닫아주어 다른 쓰레드가 작업을 하지 못하게 한다.

AutoResetEvent.Set은 Intial State를 true로 바꾸는 즉, 작업을 마치고 문을 여는 기능을 한다.

이 방식의 단점은 속도가 훨씬 느리다는 것이다.

위 코드는 Thread_1과 Thread_2에서 10000번씩 반복한거라 빠르게 나오지만 10만이 넘어가면 속도가 Spinlock에 비해 확연히 느려진다.


profile
POSTECH EE 18 / Living every minute of LIFE

0개의 댓글