[Onboarding] : 동기화

문승현·2022년 7월 6일
0

BeDev_1

목록 보기
6/7
post-thumbnail

코드를 작성하다 보면 여러 쓰레드가 특정 자원에 동시 접근하는 상황을 고려해야할 때가 있다.
예를 들어, 아래 코드와 같이 복수의 쓰레드가 어떤 메소드를 호출하는 경우를 가정해보자.
해당 메소드는 객체의 필드를 변경하고 읽는 작업을 수행한다.
그런데 작업이 동시에 이루어지다보니 메소드 마다의 결과가 기대했던 것과 다르다.

using System;
using System.Threading;

namespace MultiThrdApp
{
    class MyClass
    {
        private int counter = 1000;

        public void Run()
        {
            // 10개의 쓰레드가 동일 메서드 실행
            for (int i = 0; i < 10; i++)
            {
                new Thread(UnsafeCalc).Start();    
            }
        }

        // Thread-Safe하지 않은 메서드 
        private void UnsafeCalc()
        {
            // 객체 필드를 모든 쓰레드가 자유롭게 변경
            counter++;

            // 가정 : 복잡한 일을 한다
            for (int i = 0; i < counter; i++)
                for (int j = 0; j < counter; j++) ;

            // 필드값 읽기
            Console.WriteLine(counter);
        }
    }
        //출력 예:
        // 1005
        // 1005
        // 1007
        // 1006
        // 1007
        // 1008
        // 1006
        // 1010
        // 1010
        // 1010
}

이렇게 쓰레드들이 공유된 자원을 동시에 접근하는 상황을 예방하고,
각 쓰레드들이 순차적 혹은 제한적으로 접근하도록 하는 것을 동기화(Synchronization)라 한다.
동기화를 구현한 메소드나 클래스를 Thread-Safe하다고 하는데, 아래의 코드를 봐보자.

using System;
using System.Threading;

namespace MultiThrdApp
{
    class MyClass
    {
        private int counter = 1000;

        // lock문에 사용될 객체
        private object lockObject = new object();

        public void Run()
        {
            // 10개의 쓰레드가 동일 메서드 실행
            for (int i = 0; i < 10; i++)
            {
                new Thread(SafeCalc).Start();    
            }
        }

        // Thread-Safe 메서드 
        private void SafeCalc()
        {
            // 한번에 한 쓰레드만 lock블럭 실행
            lock (lockObject)
            {
                // 필드값 변경
                counter++;

                // 가정 : 다른 복잡한 일을 한다
                for (int i = 0; i < counter; i++)
                    for (int j = 0; j < counter; j++) ;

                // 필드값 읽기
                Console.WriteLine(counter);
            }
        }

        //출력 예:
        // 1001
        // 1002
        // 1003
        // 1004
        // 1005
        // 1006
        // 1007
        // 1008
        // 1009
        // 1010
    }
}

출력 예에서 알 수 있듯이 기대했던 대로 메소드가 동작하는 것을 확인할 수 있다.
쓰레드 동기화를 위하여 .NET에서는 아래와 같이 많은 클래스와 메소드들을 제공하고 있다.
그 중 lock과 Mutex, Semaphore에 대해서 간단히 살펴보자.

(1) Locking으로 공유 리소스에 대한 접근을 제한하는 방식
    : C# lock, Monitor, Mutex, Semaphore, SpinLock, ReaderWriterLock

(2) 타 쓰레드에 신호(Signal)을 보내 쓰레드 흐름을 제어하는 방식
    : AutoResetEvent, ManualResetEvent, CountdownEvent

lock은 특정 블럭의 코드(Critical Section)를 한번에 하나의 쓰레드만 실행할 수 있도록 해준다.
파라미터로는 임의의 객체를 사용할 수 있는데, 주로 object 타입의 private 필드를 지정한다.
위의 예시처럼 private object lockObject = new object() 와 같이 private 필드를 생성한 후,
Critical Section 시작 지점에 lock(lockObject)를 작성하는 방식으로 사용하면 된다.

Mutex 역시 lock과 마찬가지로 한번에 하나의 쓰레드만 실행할 수 있도록 해준다.
특이한 점은 쓰레드 ID를 적용해 해당 Mutex를 획득한 쓰레드만 Mutex를 해제할 수 있다.

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

namespace MultiThrdApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // 2개의 쓰레드 실행
            Thread t1 = new Thread(() => MyClass.AddList(10));
            Thread t2 = new Thread(() => MyClass.AddList(20));
            t1.Start();
            t2.Start();

            // 2개의 쓰레드 실행완료까지 대기
            t1.Join();
            t2.Join();

            // 메인쓰레드에서 뮤텍스 사용
            using (Mutex m = new Mutex(false, "MutexName1"))
            {
                // 뮤텍스를 취득하기 위해 10 ms 대기
                if (m.WaitOne(10))
                {
                    // 뮤텍스 취득후 MyList 사용
                    MyClass.MyList.Add(30);
                }
                else
                {
                    Console.WriteLine("Cannot acquire mutex");
                }
            }

            MyClass.ShowList();
        }
    }

    public class MyClass
    {
        // MutexName1 이라는 뮤텍스 생성
        private static Mutex mtx = new Mutex(false, "MutexName1");

        // 데이타 멤버
        public static List<int> MyList = new List<int>(); 

        // 데이타를 리스트에 추가
        public static void AddList(int val)
        {
            // 먼저 뮤텍스를 취득할 때까지 대기
            mtx.WaitOne();

            // 뮤텍스 취득후 실행 블럭
            MyList.Add(val);         
   
            // 뮤텍스 해제
            mtx.ReleaseMutex();
        }

        // 리스트 출력
        public static void ShowList()
        {
            MyList.ForEach(p => Console.WriteLine(p));
        }
    }
}

Semaphore는 자원에 대해 지정된 수의 쓰레드들만 접근할 수 있게 허용한다.
lock과 Mutex와 달리 복수 개의 쓰레드 접근을 허용한다는 점에서 차이가 있다.
또한 Mutex와 달리 쓰레드 ID를 적용하지 않는다.

using System;
using System.Threading;

namespace MultiThrdApp
{
    class Program
    {
        static void Main()
        {
            MyClass c = new MyClass();

            // 10개 쓰레드들 실행
            // 처음 5개만 먼저 실행되고 하나씩 해제와 함께
            // 실행될 것임.
            for (int i = 1; i <= 10; i++)
            {
                new Thread(c.Run).Start(i);
            }
        }
    }

    class MyClass
    {
        private Semaphore sema;

        public MyClass()
        {
            // 5개의 쓰레드만 허용
            sema = new Semaphore(5, 5);
        }

        public void Run(object seq)
        {
            // 쓰레드가 가진 데이타(일련번호)
            Console.WriteLine(seq);

            // 최대 5개 쓰레드만 아래 문장 실행
            sema.WaitOne();
            
            Console.WriteLine("Running#" + seq);
            Thread.Sleep(500);
            
            // Semaphore 1개 해체. 
            // 이후 다음 쓰레드 WaitOne()에서 진입 가능
            sema.Release();
            
        }
    }
}

개별 클래스와 메소드들에 대한 추가적인 내용은 아래 참고 자료들이 큰 도움이 되었다.

참고 자료 1) - 쓰레드 동기화
참고 자료 2) - Overview of Synchronization Primitive

0개의 댓글