개발언어/C#

[C#] 스레드 동기화(Thread synchronization)-Monitor

내꺼블로그 2024. 5. 17. 13:24

이번에는 스레드 동기화로 monitor 클래스에 대해 알아보도록 하자.

 

monitor는 lock처럼 크리티컬 섹션(critical section)을 locking하는 기능을 제공하고 있다. monitor를 사용하여 동기화하는 방법으로 Enter()와 Exit(), Wait()와 Pulse()/PulseAll()이 있다.

 


 

Enter(), Exit()

Enter()는 지정된 개체를 locking하는 기능을, Exit()는 지정된 개체의 lock을 해제하는 기능을 제공한다. 실제로 Monitor.Enter()과 Monitor.Exit()는 C#의 lock 기능과 동일하다.

 

 

using System;
using System.Threading;

namespace TestThread
{
    public class Account
    {
        public enum Func
        {
            Deposit, Withdraw
        }

        int money = 10000;
        public int Money { get { return money; } }

        public object objLock = new object();

        public void UseBank(Func func, object name, object money)
        {
            Monitor.Enter(objLock);
            try
            {
                switch (func)
                {
                    case Func.Deposit:
                        Deposit(name, money); break;
                    case Func.Withdraw:
                        Withdraw(name, money); break;
                }
                this.Print();
                Thread.Sleep(100);
            }
            finally { Monitor.Exit(objLock); }
        }

        public void Deposit(object name, object money)
        {
            lock (objLock)
            {
                this.money += (int)money;
                Console.WriteLine($"{name}님이 {money}원 입금하셨습니다.");
                this.Print();
                Thread.Sleep(100);
            }
        }

        public void Withdraw(object name, object money)
        {
            lock (objLock)
            {
                if (this.money < (int)money)
                {
                    Console.WriteLine("잔돈이 부족하여 돈을 출금할 수 없습니다.");
                    this.Print();
                    Thread.Sleep(100);
                    return;
                }
                this.money -= (int)money;
                Console.WriteLine($"{name}님이 {money}원 출금하셨습니다.");
                this.Print();
                Thread.Sleep(100);
            }
        }

        private void Print()
        {
            Console.WriteLine($"금액: {money}");
        }
    }

    public struct Person
    {
        public string name;

        public Person(string name) { this.name = name; }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            Account ac = new Account();
            Person p1 = new Person("홍길동");
            Person p2 = new Person("장발장");
            Person p3 = new Person("흥부");
            Person p4 = new Person("놀부");
            Person p5 = new Person("이몽룡");
            Person p6 = new Person("이세돌");

            new Thread(() => { ac.Withdraw( p1.name, 4000); }).Start();
            new Thread(() => { ac.Deposit( p2.name, 10000); }).Start();
            new Thread(() => { ac.Deposit(p3.name, 2000); }).Start();
            new Thread(() => { ac.Deposit(p4.name, 3000); }).Start();
            new Thread(() => { ac.Withdraw(p5.name, 6000); }).Start();
            new Thread(() => { ac.Deposit(p6.name, 5000); }).Start();
        }
    }
}

 

이전 포스팅에서 lock으로 구현했던 부분을 Enter, Exit로 바꿔서 구현하였다. Enter를 사용하여 크리티컬 섹션(try 안에 있는 구문)에 스레드 하나만 들어가도록 locking 한다. 스레드가 완료되면 Exit를 사용하여 lock을 해제하고 다음 스레드가 들어가도록 한다.

보면 알다시피 lock은 Enter와 Exit의 간략한 형태이다.

 

결과가 잘 나온 것을 볼 수 있다.


 

Wait(), Pulse()/PulseAll()

lock을 점유하고 있는 스레드가 Wait()를 호출할 경우, 해당 스레드는 lock이 해제되고 대기 큐에 들어간다. lock이 해제된 상태이므로 준비 큐의 첫번째 스레드가 lock을 다시 점유하고 크리티컬 섹션에 들어간다. 대기 큐의 첫번째 스레드는 lock을 점유한 스레드에 의해 호출되거나 Pulse()가 호출될 때 대기 큐에서 준비 큐로 이동한다. PulseAll()가 호출될 경우 대기 큐에 있는 모든 스레드는 준비 큐로 이동하게 된다.

 

 

Wait()와 Pulse()를 사용할 때 주의사항은 이 두 메서드는 반드시 lock 안에서 호출되어야 한다는 점이다.

 

 

Wait()와 Pulse()를 이용해서 다음 코드를 구현해보았다.

4개의 스레드가 각각 10개씩 번갈아가며 100까지 count하도록 구현하였다.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;

namespace TestThread
{
    public class Func
    {
        int count = 1;
        Queue<int> q = new Queue<int>();
        object obj = new object();
        public void Run(object n)
        {
            lock (obj)
            {
                while (count <= 100)
                {
                    
                    Console.WriteLine($"({n})Thread: {count}");
                    while (count % 10 == 0)
                    {
                        Console.WriteLine($"wait ({n})Thread: {count}");
                        ++count;
                        q.Enqueue(count);
                        Monitor.Wait(obj);
                    }
                    ++count;
                    while (q.Count > 0)
                    {
                        q.Dequeue();
                        Monitor.Pulse(obj);
                        --count;
                    }   
                    Thread.Sleep(100);
                }
            }
        }
    }
    internal class Program
    {
        static void Main(string[] args)
        {
            Func f = new Func();
            Thread th1 = new Thread(f.Run);
            Thread th2 = new Thread(f.Run);
            Thread th3 = new Thread(f.Run);
            Thread th4 = new Thread(f.Run);
            //Thread.Sleep(5000);
            th1.Start(1);
            th2.Start(2);
            th3.Start(3);
            th4.Start(4);
        }
    }
}

 

 

lock으로 해당 크리티컬 섹션을 잠금해준다.

while문을 lock 안에 넣은 이유는 lock밖에 넣었을 경우 count가 100인 시점에서 스레드 4개가 다 들어가서 count를 103까지 출력하기 때문...(처음에 103까지 나오는거 보고 당황했었다...)

그리고 count를 저렇게 기괴하게 해놓은 데는 나름의 이유가 있다.

 

첨에 이렇게만 했을 때는

 

이런 식으로 멈춰버리더이다...

while문에서 다 먹혀버리는듯...

wait에 해당하는 부분에도 count를 증가해야만 했다.

 

count를 추가했더니

끝까지는 되는데 count가 이상하다.

원인을 살펴보니

while에 계속 걸리지 않도록 count했던게 wait 풀리고 while문 빠져나가면서 count를 한번 더 해서 일어난 결과였다.

이에 생각해낸 방법은 스레드를 Wait할에만 count를 감소하면서 Pulse하도록 하는 것. 그리고 그것이 현재의 코드이다.

 

결과

결과가 잘 나오는 것을 볼 수 있다.

 

그러다 문득 든 생각, while에 계속 걸려서 발생한 일이라면 if를 쓰면 되지 않을까?

원하던 결과가 잘 나온다..

그치만 wait 조건을 계속 살피는게 맞는거니까(?) 사실 while 쓰는게 맞는거긴 하지 않을까 싶댱

 


 

참고 사이트