공부/게임서버

Monitor 사용 시 주의사항

samosa 2024. 11. 13. 23:16

number++number-- 연산이 각각 다른 함수에서 실행될 때, 이 코드가 다중 스레드 환경에서 경합 조건(race condition)을 일으킬 수 있는지, 그리고 이를 상호배제(Mutual Exclusion)로 해결하는 방법을 예시로 설명하겠다.

문제 상황: 경합 조건 발생

다음은 두 개의 함수가 공유 자원 number에 동시에 접근하는 예시이다.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace RaceConditionExample
{
    internal class Program
    {
        private static int number = 0;

        private static void Increment()
        {
            for (int i = 0; i < 1000000; i++)
            {
                number++; // 원자적이지 않은 연산
            }
        }

        private static void Decrement()
        {
            for (int i = 0; i < 1000000; i++)
            {
                number--; // 원자적이지 않은 연산
            }
        }

        private static void Main(string[] args)
        {
            var t1 = new Task(Increment);
            var t2 = new Task(Decrement);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine($"Final value of number: {number}");
        }
    }
}

문제 설명

  • number++number--는 각각 메모리에서 값을 읽고, 수정하고, 다시 저장하는 과정으로 이루어진다. 이 과정은 여러 단계로 이루어져 있어 원자적이지 않다.
  • 두 스레드가 동시에 number에 접근하면 예기치 않은 결과가 발생할 수 있다. 예를 들어, t1number++를 수행하는 중에 t2number--를 수행하면 중간 값이 손실될 수 있다.

해결 방법: 상호배제를 통한 임계영역 보호

Monitor 클래스를 사용하여 number 변수에 대한 접근을 보호하는 코드를 작성할 수 있다.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace CriticalSectionExample
{
    internal class Program
    {
        private static int number = 0;
        private static readonly object _lockObject = new object();

        private static void Increment()
        {
            for (int i = 0; i < 1000000; i++)
            {
                Monitor.Enter(_lockObject); // 상호배제를 위한 잠금
                try
                {
                    number++;
                }
                finally
                {
                    Monitor.Exit(_lockObject); // 잠금 해제
                }
            }
        }

        private static void Decrement()
        {
            for (int i = 0; i < 1000000; i++)
            {
                Monitor.Enter(_lockObject); // 상호배제를 위한 잠금
                try
                {
                    number--;
                }
                finally
                {
                    Monitor.Exit(_lockObject); // 잠금 해제
                }
            }
        }

        private static void Main(string[] args)
        {
            var t1 = new Task(Increment);
            var t2 = new Task(Decrement);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine($"Final value of number: {number}");
        }
    }
}

설명

  • Monitor.Enter(_lockObject)Monitor.Exit(_lockObject)를 사용하여 number 변수에 대한 접근을 보호한다.
  • try-finally 블록을 사용하여 Monitor.Exit이 항상 호출되도록 보장한다. 예외가 발생하더라도 잠금이 해제되어 다른 스레드가 접근할 수 있게 한다.
  • 이 코드를 실행하면 IncrementDecrement 함수가 동시에 실행되더라도, number 변수에 대한 접근이 상호배제를 통해 보호되어 올바른 값이 유지된다.

이 방식으로 number 변수에 대한 동시 접근이 안전하게 이루어져 경합 조건을 방지할 수 있다.

 


그러나 사실상 lock을 쓰지, Monitor를 쓰지는 않는다고 함.