개발언어/C#

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

내꺼블로그 2024. 5. 16. 14:06

스레드는 비동기식으로 여러 스레드가 동시에 실행될 수 있다. 비동기란 처리할 작업을 요청하였을 때, 결과를 반환받지 않아도 다른 작업들을 동시에 수행하는 것을 의미한다. (반대로 동기는 요청한 작업의 결과를 받을 때까지 다른 작업을 수행하지 않는 것을 의미한다.)

 

비동기로 수행될 경우, 동시에 여러 작업들을 수행할 수 있어 성능상의 이점이 있지만, 여러 작업들이 공유 자원에 접근할 때 오류가 발생할 수 있다.

 

모임통장을 사용한 예를 들어보자. 여섯명이 동시에 다음과 같은 일을 수행하였다고 하자.

using System;
using System.Threading;

namespace TestThread
{
    public class Account
    {
        int money = 10000;
        public int Money { get { return money; } }

        public void Deposit(object name, object money)
        {
            this.money += (int)money;
            Console.WriteLine($"{name}님이 {money}원 입금하셨습니다.");
            Console.WriteLine($"금액: {this.money}");
        }

        public void Withdraw(object name, object money)
        {
            if (this.money < (int)money)
            {
                Console.WriteLine("잔돈이 부족하여 돈을 출금할 수 없습니다.");
                Console.WriteLine($"금액: {this.money}");
                return;
            }
            this.money -= (int)money;
            Console.WriteLine($"{name}님이 {money}원 출금하셨습니다.");
            Console.WriteLine($"금액: {this.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); Thread.Sleep(1000); }).Start();
            new Thread(() => { ac.Deposit(p2.name, 10000); Thread.Sleep(1000); }).Start();
            new Thread(() => { ac.Deposit(p3.name, 2000); Thread.Sleep(1000); }).Start();
            new Thread(() => { ac.Deposit(p4.name, 3000); Thread.Sleep(1000); }).Start();
            new Thread(() => { ac.Withdraw(p5.name, 6000); Thread.Sleep(1000); }).Start();
            new Thread(() => { ac.Withdraw(p6.name, 5000); Thread.Sleep(1000); }).Start();
        }
    }
}

 

 

결과는 다음과 같이 엉망이다. 여럿이서 동시에 공유자원을 이용하면서 값에 대한 갱신이 제대로 이루어지지 않아 발생한 문제이다.(금액을 찍기도 전에 다른 쓰레드에서도 공유자원 값을 변경해서 변경된 금액이 찐힌 거..)

이처럼 공유자원을 여럿이서 사용할 경우에는 동기화가 필요한 법이다.

 


스레드 동기화 방법: 1.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)
        {
            lock (objLock)
            {
                switch (func)
                {
                    case Func.Deposit:
                        Deposit(name, money); break;
                    case Func.Withdraw:
                        Withdraw(name, money); break;
                }
                this.Print();
                Thread.Sleep(100);
            }
        }

        public void Deposit(object name, object money)
        {

            this.money += (int)money;
            Console.WriteLine($"{name}님이 {money}원 입금하셨습니다.");
        }

        public void Withdraw(object name, object money)
        {

            if (this.money < (int)money)
            {
                Console.WriteLine("잔돈이 부족하여 돈을 출금할 수 없습니다.");
                return;
            }
            this.money -= (int)money;
            Console.WriteLine($"{name}님이 {money}원 출금하셨습니다.");
        }

        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.UseBank(Account.Func.Withdraw, p1.name, 4000);  }).Start();
            new Thread(() => { ac.UseBank(Account.Func.Deposit, p2.name, 10000);  }).Start();
            new Thread(() => { ac.UseBank(Account.Func.Deposit, p3.name, 2000); }).Start();
            new Thread(() => { ac.UseBank(Account.Func.Deposit, p4.name, 3000); }).Start();
            new Thread(() => { ac.UseBank(Account.Func.Withdraw, p5.name, 6000); }).Start();
            new Thread(() => { ac.UseBank(Account.Func.Deposit, p6.name, 5000); }).Start();
        }
    }
}

 

 

lock은 해당 메서드를 사용하는 스레드(현재 코드에서는 objLock이다.)가 종료될 때까지 다른 스레드는 접근하지 못하도록 막는 기능을 한다.

 

오브젝트는 클래스 안, 메서드 밖에서 만들어야 한다. 그 이유는 메서드 안에서 오브젝트를 만들면 메서드를 호출할 때마다 생성된 스레드 각각에서 오브젝트가 새로 만들어지므로 lock이 무용지물이 된다... 동기화할 메서드끼리는 하나의 오브젝트로 lock 사용하기..

 

 

결과가 제대로 나온 모습을 볼 수 있다.

 


 

참고 사이트: 사이트1 / 사이트2