저 하늘의 구름처럼~

C# lock 그리고 Monitor.Enter/Monitor.Exit 본문

DotNet

C# lock 그리고 Monitor.Enter/Monitor.Exit

강백호v 2015. 12. 6. 19:44

lock은 thread-unsafe(안전하지 않은 스레드)코드를 안전하게 사용하고자 할때 사용한다. 즉, 동기화 처리를 할때 사용하는 구문이다.

그렇다면, thread-unsafe한 코드는 어떤 코드인가 다음 예제를 보자.

       static public class Division
        {
            static int num1 = 100, num2 = 5;

            static public void Divide()
            {
                try
                {
                    if (num2 != 0) Console.WriteLine(num1 / num2);
                    num2 = 0;
                }
                catch (Exception e)
                {
                    Debug.WriteLine(string.Format("Err : {0}", e));
                }
            }
        }

 

 

Divide 함수가 스레드 하나의 함수에 의해 호출되면 안전하게 동작하겠지만, 두개 이상의 스레드에서 동시에 실행되면 thread-unsafe한 상태가 된다.

첫번째 실행된 스레드가 num2 = 0으로 할당하는 시점에 두번째 실행된 스레드가  num1/num2를 수행하게되면 DivideByZeroException("0으로 나누려 했습니다.")을 발생시키면서 정상 동작을 못하는 상황이 발생될 수 있기때문이다.

thread-safe(스레드에 안전한 코드)로 만들려면 아래 클래스와 같이 수정해주면된다.

    static public class Division
    {
        static int num1 = 100, num2 = 5;
        static readonly object divisionlocker = new object();

        static public void Divide()
        {
            try
            {
                lock (divisionlocker)
                {
                   if (num2 != 0) Console.WriteLine(num1 / num2);
                    num2 = 0;
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine(string.Format("Err : {0}",e));
            }
        }
    }

 

lock 구문에 의해 첫번째로 실행된 스레드의 코드 처리 위치가 lock 구문안에 있다면, 두번째로 실행된 스레드는 첫번째 실행된 스레드가 lock구문 밖으로 나올때까지 기다렸다가 실행하여 thread-safe하게 동작한다.

Division 클래스에서 사용한 lock구문을 Monitor.Enter/Monitor.Exit로 구현한다면, 다음과 같다.

    Monitor.Enter(divisionlocker);
    try
    {
        if (num2 != 0) Console.WriteLine(num1 / num2);
        num2 = 0;
     }
     finally { Monitor.Exit(divisionlocker); }

실제로, C# 1.0 ~ 3.0에서는 lock구문을 사용하면 컴파일러가 자동으로  Monitor.Enter/Exit 함수를 사용하여 위와 같은 형태로 바꿔주는 동작을 수행한다.

그런데, 이 코드는 약간의 문제가 있다. 만약 Monitor.Enter내부에서 Exception이 발생하거나, Monitor.Enter에 진입하고 try 구문 진입하기 직전에 외부에서 thread를 abort시킨다거나 OutOfMemoryException이 발생한다면 lock은 수행했지만, lock을 풀어줄 방법은 사라지기 때문에 leak이 발생하게 된다.

이러한 문제점을 보완하기 위해 CLR 4.0에서는 다음과 같은 Monitor.Enter함수를 overload하여 제공한다.

public static void Enter (object obj, ref bool lockTaken);

Enter함수가 Exception을 throw하거나 lock을 하지못하는 상황이 발생한다면 lockTaken는 false가 된다. 아래와 같은 형태로 Monitor.Enter구문을 수정한다면 lock을 풀어줄 방법이 없는 상황은 사라지게 된다.

     bool lockTaken = false;
     try
     {
           Monitor.Enter(divisionlocker, ref lockTaken);
           if (num2 != 0) Console.WriteLine(num1 / num2);
           num2 = 0;

      }
      finally { if (lockTaken) Monitor.Exit(divisionlocker); }

 

- 참고 문헌 : http://www.albahari.com/threading/part2.aspx (Threading in C# Joseph Albahari)