Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Archives
Today
Total
05-16 04:51
관리 메뉴

nomad-programmer

[Programming/C#] Monitor 저수준 동기화 : Wait() & Pulse() 본문

Programming/C#

[Programming/C#] Monitor 저수준 동기화 : Wait() & Pulse()

scii 2020. 9. 29. 20:38

저수준 동기화 : Monitor.Wait() 메소드와 Monitor.Pulse() 메소드

lock 키워드 대신 Monitor 클래스를 사용해야 한다면 그건 Enter()와 Exit() 메소드 때문이 아니라 Wait()와 Pulse() 메소드 때문일 것이다.

Monitor.Wait() 메소드와 Monitor.Pulse() 메소드는 단순히 lock 키워드만을 사용할 때보다 더 섬세하게 멀티 스레드 간의 동기화를 가능하게 해준다.

이 두 메소드는 반드시 lock 블록 안에서 호출해야 한다. lock을 걸어 놓지 않은 상태에서 이 두 메소드를 호출한다면 CLR이 SynchronizationLockException 예외를 던지기때문이다.

  • Wait() : 스레드를 WaitSleepJoin 상태로 만든다.
  • Pulse() : 스레드를 WaitSleepJoin 상태에서 깨운다. 

Wait() 메소드는 스레드를 WaitSleepJoin 상태로 만든다. 이렇게 WaitSleepJoin 상태에 들어간 스레드는 동기화를 위해 갖고 있던 lock을 내려놓은 뒤 Waiting Queue라고 하는 큐에 입력되고, 다른 스레드가 락을 얻어 작업을 수행한다. 
작업을 수행하던 스레드가 일을 마친 뒤 Pulse() 메소드를 호출하면 CLR은 Waiting Queue의 가장 첫 요소 스레드를 꺼낸 뒤 Ready Queue에 입력시킨다.
Ready Queue에 입력된 스레드는 입력된 차례에 따라 락을 얻어 Running 상태에 들어간다. 이 말은 즉, 다시 작업을 수행한다는 것이다.

아래의 그림은 Wait() 메소드와 Pulse() 메소드를 호출할 때 일어나는 일들이다.

Thread.Sleep() 메소드도 스레드를 WaitSleepJoin 상태로 만들기는 하지만 Monitor.Pulse() 메소드에 의해 깨어날 수는 없다. 또한 Waiting Queue에 들어가지도 않는다. 다시 Running 상태로 돌아오려면 매개 변수로 입력된 시간이 경과되거나 인터럽트 예외를 받아야 깨어난다.

Monitor.Wait() 메소드는 Monitor.Pulse() 메소드가 호출되면 바로 깨어날 수 있다. 이 때문에 멀티 스레드 애플리케이션의 성능 향상을 위해서 Monitor.Wait() 메소드와 Monitor.Pulse() 메소드를 사용하는 것이다.

Monitor.Wait() & Monitor.Pulse() 메소드 사용방법

step1. 클래스 안에 다음과 같이 동기화 객체 필드를 선언한다.

readonly object thisLock = new object();

step2. 스레드를 WaitSleepJoin 상태로 바꿔 블록시킬 조건 (Wait()를 호출할 조건)을 결정할 필드를 선언한다.

bool lockedCount = false;

step3. 스레드를 블록시키고 싶은 곳에서 다음과 같이 lock 블록 안에서 step2에서 선언한 필드를 검사하여 Monitor.Wait() 메소드를 호출한다.

lock (thisLock)
{
    while (lockedCount == true)
    {
        Monitor.Wait(thisLock);
    }
}

step4. step3에서 선언한 코드는 lockedCount가 true이면 해당 스레드는 블록된다. step2에서 선언한 lockedCount의 값을 true로 변경한다. 이렇게 해두면 다른 스레드가 이 코드에 접근할 때 step3에서 선언해둔 블로킹 코드에 걸려 같은 코드를 실행할 수 없게 된다.
작업을 마치면 lockedCount의 값을 다시 false로 바꾼 뒤 Monitor.Pulse()를 호출한다. 그럼 WaitingQueue에 대기하고 있던 다른 스레드가 깨어나서 false로 바꾼 lockedCount를 보고 작업을 수행할 것이다.

lock (thisLock)
{
    while (lockedCount == true)
    {
        Monitor.Wait(thisLock);
    }
    
    lockedCount = true;
    count++;
    lockedCount = false;
    
    Monitor.Pulse(thisLock);
}

Wait() 메소드와 Pulse() 메소드의 사용 예

using System;
using System.Threading;

namespace CSharpExample
{
    class Counter
    {
        private const int LOOP_COUNT = 1000;
        private readonly object thisLock;
        private bool lockedCount = false;

        public int Count { get; set; }

        public Counter()
        {
            thisLock = new object();
            Count = 0;
        }

        public void Increase()
        {
            int loopCount = LOOP_COUNT;
            while (loopCount-- > 0)
            {
                lock (thisLock)
                {
                    // count가 0보다 크거나 lockedCoutn가 다른 스레드에 의해 true로 바뀌어 있으면
                    // 현재 스레드를 블록시킨다. 다른 스레드가 Pulse() 메소드를 호출해 줄 때까지
                    // WaitSleepJoin 상태도 남는다.
                    while (Count > 0 || lockedCount == true)
                    {
                        Monitor.Wait(thisLock);
                    }
                    lockedCount = true;
                    Count++;
                    lockedCount = false;

                    Monitor.Pulse(thisLock);
                }
                Thread.Sleep(1);
            }
        }

        public void Decrease()
        {
            int loopCount = LOOP_COUNT;
            while (loopCount-- > 0)
            {
                lock (thisLock)
                {
                    while (Count < 0 || lockedCount == true)
                    {
                        Monitor.Wait(thisLock);
                    }
                    lockedCount = true;
                    Count--;
                    lockedCount = false;

                    Monitor.Pulse(thisLock);
                }
                Thread.Sleep(1);
            }
        }

    }
    internal class MainApp
    {
        static int Main(string[] args)
        {
            Counter counter = new Counter();

            Thread incThread = new Thread(new ThreadStart(counter.Increase));
            Thread decThread = new Thread(new ThreadStart(counter.Decrease));

            incThread.Start();
            decThread.Start();

            incThread.Join();
            decThread.Join();

            Console.WriteLine(counter.Count);

            return 0;
        }
    }
}


/* 결과

0

*/
Comments