no image
[c#/유니티] DeadLock과 SpinLock에 대해
DeadLock에 대해 알아보자 📌  데드락(DeadLock)이란?              두 개 이상의 프로세스가 서로의 작업이 끝나기만을 기다림. 둘 다 대기상태에 이르러 영원히 끝나지 않은 상황이다.              교착 상태라고도 말한다.                          👉 한정된 자원을 얻기 위해 서로 경쟁하기 때문에 발생한다.           🔎  DeadLock의 4가지 필요 조건                                아래 4가지 조건이 모두 만족되는 경우, 데드락이 발생하라 가능성이 있음                하나라도 만족하지 않으면 절대로 발생하지 않는다. -> 하나라도 해결되면, 데드락 문제를 풀 수 있다.                 ..
2022.12.08
[c#/유니티] 임계영역(Critical Section)을 위한 Monitor와 Lock
멀티 스레드 환경에서 같은 객체를 여러 곳에서 호출하는 경우 예기치 않은 결과가 나타날 확률이 높다. 📌 Monitor 활용 상호 배제(Mutual Exclusive)를 이뤄 다른 스레드의 접근을 막도록 한다. Enter과 Exit로 구현하며 Enter는 문을 잠그는 행위, Exit는 문을 여는 행위라 생각하면 된다. Thread 1에서 Monitor.Enter(_obj)를 통해 임계 영역(Critical Section)에 들어가면, Thread2는 Thread 1에서 Monitor.Exit(obj)로 해제할 때 까지 해당 영역에 접근하지 못한다. *임계 영역 : 여러 스레드가 공유 자원에 접근할 때, 하나만 접근할 수 있도록 보장해 주는 영역. static int number = 0; //공유 자원 s..
2022.12.07
no image
[c#/유니티] Interlocked에 대해
* Interlocked란? int 형 값을 증가시키거나 감소시키는 데 사용하는 클래스 이다. 원자성과 순서를 보장해준다. (단점 : 성능에서 손해) 다음과 같이 int 형 전역 변수인 number를 공유하고 있는 상황에서 Thread_1과 Thread_2가 number를 증가시키거나 감소시키는 작업을 하려 한다. static int number = 0; //전역변수 static void Thread_1() { for (int i = 0; i < 100000; i++) number++; } static void Thread_2() { for (int i = 0; i < 100000; i++) number--; } 결과값으로 0을 기대 했지만, 이상한 값이 나오는 것을 볼 수 있다. 왜..? 🤔 threa..
2022.12.07
no image
[c#/유니티] 캐시(Cache) 이론과 메모리 배리어(Memory Barrier)
* 캐시(Cache)란? 자주 사용하는 데이터나 값을 미리 복사해 둔 임시 장소이다. 저장공간이 작고 비용이 비싼 대신 빠른 성능 제공 장점 : 데이터를 미리 복사해 놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터 접근이 가능함. 즉, DBMS 혹은 서버에 요청하기 보다는 메모리에 데이터를 저장하여 불러다 쓴다. -> DBMS의 부하가 줄어들며 성능을 높이기 위해서 사용 * 캐시(Cache) 철학 1) 시간 지역성(temporal locality) : 가장 최근에 사용한 변수가 또 사용될 확률이 높다 2) 공간 지역성(spacial locality) : 방금 접근한 주소와 인접한 주소의 변수가 사용될 확률이 높다 int[,] arr = new int[10000, 10000]; { long now ..
2022.12.06
no image
[c#/유니티] 컴파일러 최적화
스레드에 이어서... 다음과 같이 스레드를 작성하고 debug 모드로 실행하면 정상적으로 실행되는 것을 볼 수 있다. using System; using System.Threading; using System.Threading.Tasks; namespace ServerCore { class Program { static bool _stop = false; //모든 스레드가 동시에 접근함 static void ThreadMain() { Console.WriteLine("쓰레드 시작!"); //단축키 : cw를 치고 tab 두번 누르기 while(_stop == false) { //누군가가 stop 신호를 해주기를 기다림 } Console.WriteLine("쓰레드 종료!"); } static void Ma..
2022.12.06
[c#/자료구조] 배열(Array)
자료구조 분류 단순 구조(Primitive Data Structure) : 기본적인 데이터 타입. 정수, 실수, 문자, 부울린 등의 기초 타입들 선형 구조(Linear Data Structure) : 자료가 선형적으로 연결되어 있는 구조. 배열, 연결리스트, 스택, 큐 비선형 구조(Non-linear Structure) : 자료 간 관계가 1대 다 혹은 다대다 구조. 네트워크 혹은 계층구조를 가짐. 트리, 그래프 파일 구조 : 레코드의 집합인 파일에 대한 자료구조. 순차파일, 색인 파일, 직접파일.. 1. 배열이란? ▶ 배열 : 연속적인 메모리 상에 동일한 데이터 타입의 요소들을 순차적으로 저장하는 자료구조 - 각 요소마다 인덱스를 갖고 있음 - 크기가 고정되어 있음 1차원 배열 선언 Int[] A = ..
2022.12.06
이것 저것..
안드로이드 하다 웹 개발 하다 플러터 앱 만들다, 웹 유지보수 하다가 이제는 게임 서버를 하게 됐다 이것 저것 시도만 하는 것 처럼 보일 수 있지만.. 나 스스로 경계와 한계를 짓지 않고 지금 맡은 것에 최선을 다하기로 했다.. 배우고 싶은 것도 많고 알고 싶은 것도 많기에 꾸준히 다양한 분야에 대해 공부할 것이다. 언어만 다를 뿐, 결국 다 컴퓨터와 소통하는 방법이다. 요즘 관심이 가장 많은 것은 IOS 개발! 애플 아카데미에도 지원했다 후에 좋은 결과가 있길...😊
2022.12.06
no image
[c#/유니티] 스레드(Thread)와 스레드풀(ThreadPool)
프로세스와 스레드에 대해 먼저 알아보자. 프로세스 : 현재 실행 중인 프로그램 스레드 : 운영 체제가 프로세서 시간을 할당하는 기본 단위 / 명령어를 실행하기 위한 스케쥴링 단위 쉽게 말해서, 스레드는 작업자라고 생각하면 된다. 어떠한 업무가 주어졌을 때, 그 일을 수행하는 작업자가 바로 스레드 이다. * NameSpace(네임스페이스) 선언하기 using System.Threading; 스레드를 사용하기 위해서 다음과 같이 선언해주자 1) 스레드 선언하기 Thread t = new Thread(MainTread); 스레드를 사용하기 위해 다음과 같이 스레드 생성자를 만든다. 넘겨주는 MainTread 함수 또한 생성한다. static void MainTread(object state) { Console..
2022.12.05
320x100
728x90

DeadLock에 대해 알아보자

 

📌  데드락(DeadLock)이란?

              두 개 이상의 프로세스가 서로의 작업이 끝나기만을 기다림. 둘 다 대기상태에 이르러 영원히 끝나지 않은 상황이다.

              교착 상태라고도 말한다. 

 

                        👉 한정된 자원을 얻기 위해 서로 경쟁하기 때문에 발생한다. 

 

 

 

       🔎  DeadLock의 4가지 필요 조건

               

                아래 4가지 조건이 모두 만족되는 경우, 데드락이 발생하라 가능성이 있음

                하나라도 만족하지 않으면 절대로 발생하지 않는다. -> 하나라도 해결되면, 데드락 문제를 풀 수 있다. 

 

 

                  •   상호 배제(Mutual Exclusion)

                            하나의 자원은 한 번에 한 프로세스만 사용할 수 있다. 임계 영역(Critical Section)을 뜻한다. 

 

                 점유와 대기(Hold and wait)

                            어떤 프로세스가 하나 이상의 자원을 점유하고 있으면서 다른 프로세스가 가지고 있는 자원을 기다림 

 

                 • 비선점(No Preemption)

                            프로세스가 작업을 마친 후, 자원을 자발적으로 반환할 때 까지 기다림(강제로 뺏지 않는다)

                            다른 프로세스 혹은 스레드가 획득한 자원이 반환될 때 까지 기다림을 뜻한다. 

 

                 • 환형 대기(Circular Wait)

                            Hold and Wait 관계의 프로세스들이 서로를 기다림 

                            T1(A -> B), T2(B -> C), T3(C -> D), T4(D -> A) 와 같이 서로가 물려있는 상태를 뜻한다. 

 

 

 

 

      💥  해결 방법

 

                 1. 상호배제 제거 

                         자원 공유를 가능하게 한다 -> 거의 불가능 

                         싱글 스레드거나 공유 데이터가 없으면 상호배제 영역이 제거 된다.

 

                2. 점유대기 제거

                         lock 거는 코드를 제거한다. 프로세스 실행 전 모든 자원을 할당한다. 

 

                3. 비선점 제거 

                        이미 걸려있는 lock을 임의로 해제한다. 

                        자원 점유 중인 프로세스가 다른 자원을 요구할 때 가진 자원을 보내도록 한다. 

 

                4. 순환대기 제거 

                        lock에 방향성을 넣어, 하나의 방향으로만 lock을 걸도록 처리한다. 

 

 

 

 

           코드를 보며 이해해 보자. 

class SessionManager
{
    static object _lock = new object();

    public static void TestSession()
    {
        lock (_lock)
        {
            // SessionManager의 작업을 여기서 수행
        }
    }

    public static void Test()
    {
        lock (_lock)
        {
            UserManager.TestUser();
        }
    }
}

class UserManager
{
    static object _lock = new object();

    public static void Test()
    {
        lock (_lock)
        {
            SessionManager.TestSession();
        }
    }

    public static void TestUser()
    {
        lock (_lock)
        {
            // UserManager의 작업을 여기서 수행
        }
    }
}

 

 

       SessionManagerUserManager 클래스의 Test 메소드는 각각의 고유한 TestSession과 TestUser를 호출하고 있다. 

       SessionManager를 먼저 보면, Test의 크리티컬 섹션에서 UserManager의 TestUser를 실행하려고 호출했지만,

       이미 TestUser 메소드lock에 걸려 선점된 상태이다.  SessionManager는 선점된 상태를 그저 바라볼 뿐이다.

 

 

       UserManager에서의 Test 메소드에서 또한 SessionManager의 TestSession 메소드를 호출했다.

       하지만 해당 메소드는 이미 선점되어서 lock이 걸린 형태이다. UserManager도 이를 바라 볼 뿐이다.

 

       이렇게 서로의 작업이 끝나기만을 기다리는 상태가 교착상태(DeadLock)이다. 

 

        쉽게 말해서, A는 나이프를 선점한 상태(갖고 있는)에서 B에게 포크를 달라 하고 있고, B는 포크를 선점한 상태(갖고 있는)          에서 A에게 나이프를 달라고 하며 서로 대기하는 상태인 것이다. 

 

 

 

 

 

 

 

 

📌   SpinLock 이란?

             lock은 상호 배타적 특성을 구현하는 방법이라 할 수 있다. 

            스핀락은 락의 한 종류로 루프를 돌면서 계속 점유를 시도하는 방법을 채택하는 락이다. 

            스레드 동기화(Thread-safe)를 구현하기 위한 .NET 클래스 중 하나이다. 

           

            장점 : 문맥교환(Context Switching)이 발생하지 않아 CPU 부하를 줄일 수 있다.

                 👉 하지만 한 스레드가 Lock을 오랫동안 소유하고 있다면, 다른 스레드들은 계속해서 무한루프만 돌 것이다 ❗

                       따라서 임계 영역이 짧거나, 빨리 처리가 가능한 경우에 사용하는 것이 용이하다. 

 

 

* 상호 배타적 : 어느 한 사건이 일어났을 때, 다른 사건이 발생할 수 없음. 즉, 동시에 일어날 수 없는 사건 

스레드 동기화 : 각 스레드들이 순차적으로 혹은 제한적으로 접근하도록 하는 것.

 

 

 

 

 

 

             💥  SpinLock 구현 

 

                      1. lock을 다른 스레드가 선점하고 있으면, lock이 풀릴 때까지 무한 루프를 돈다(spin).

                      2. lock을 선점하고 있는 스레드가 없으면, 해당 lock을 얻고 lock 상태를 변경한다(소유중)

 

class SpinLock
{
    volatile bool _locked = false;

    public void Acquire()
    {
        while (_locked)
        {
            // 잠김이 풀리기를 기다린다
        }

        _locked = true;
    }

    public void Release()
    {
        _locked = false;
    }
}

 

하지만 다음과 같은 코드는 아래와 같이 제대로 된 값을 출력하지 않는 것을 볼 수 있다. 

 

잘못된 결과값 출력

 

              while문을 도는 상황에서 다른 스레스가 접근 순서를 달리할 수 있어 경쟁 상태가 발생하기 때문이다. 

 

*경쟁 상태(Race Condition) : 프로세스가 어떤 순서로 데이터에 접근하느냐에 따라 결과값이 달라질 수 있는 상황.

둘 이상의 입력이나 조작이 동시에 일어나 의도하지 않은 결과를 가져오는 경우.

 

 

 

 

 

 

 

 

          🔎  해결 방법

                     Interlocked 클래스의 Exchange() 혹은 CompareExchange()  메소드를 통해 이를 해결할 수 있다. 

 

                     

 

 

                    📌  Interlocked.Exchange(ref location, value) 메소드 

 

                              location 변수를 value 값으로 초기화 하고, 초기화 되기 전의 값(원래의 값)을 리턴한다.

 

 

                           1. _locked를 1로 계속 초기화 하며 초기화 이전 값을 original에 할당하며 이 값을 계속 확인한다.

                           2. original이 1이면 lock 이 걸려있다는 의미로 계속 반복문을 돌면서 확인하고, original이 0이면 lock이 풀                                   려 있다는 의미로 반복문을 나와 진입한다. 

 

class SpinLock
{
    volatile int _locked = 0;
    public void Acquire()
    {
        while (true)
        {
            int original = Interlocked.Exchange(ref _locked, 1); //스택에 들어 있는 값 
            if (original == 0)
                break;
        }

    }
    public void Release()
    {
        _locked = 0;
    }
}

다음과 같이 정상적으로 출력되는 것을 볼 수 있다. 

 

 

 

 

 

 

 

                   📌  Interlocked.CompareExchange(ref location, value, comparand) 메소드

 

                                location 값이 comparand와 같으면 location을 value로 초기화하고, location의 원래 값을 리턴한다. 

 

                                 👉이 방법은 CAS(Comapre-And-Swap)에 해당하며 원자성을 가지며 update된 값을 가짐을 보장한다. 

 

*CAS(Comapre-And-Swap) : 비교한 후 값을 바꾸어 주는 것 

 

                                 

 

                        1. expected는 비교하는 값, desire은 새로 교체할 값에 해당한다.

                        2. _locked와 expected가 동일하다면 _locked값을 desire 값으로 초기화 하고, _locked의 원래 값을 반환한다.

                               

                                👉 반복문을 돌면서 계속 확인하다가, expected와 _lock의 값이 같다면 _lock 값을 desire로 바꾸어주고                                        반복문을 종료하여 lock을 획득한다 라는 의미이다. 

class SpinLock
{
    volatile int _locked = 0;
    public void Acquire()
    {

        while (true)
        {
            int expected = 0;
            int desire = 1;
            //int original = Interlocked.CompareExchange(ref _locked, desire, expected);
            if (Interlocked.CompareExchange(ref _locked, desire, expected) == expected)
                break;
        }
    }

      public void Release()
      {
          _locked = 0;
      }
 }

         

            이 또한 결과값이 올바르게 나옴을 확인할 수 있다. 

          CAS를 보장하는 Interlocked.CompareExchange 방식을 주로 활용하도록 하자.

 

 

 

 

 

 

출처 및 참고 : https://itwiki.kr/w/%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9C

 https://www.inflearn.com/course/유니티-mmorpg-개발-part4

728x90
반응형
320x100
728x90

 

멀티 스레드 환경에서 같은 객체를 여러 곳에서 호출하는 경우

예기치 않은 결과가 나타날 확률이 높다. 

 

 

 

 📌  Monitor 활용

             상호 배제(Mutual Exclusive)를 이뤄 다른 스레드의 접근막도록 한다

             Enter Exit로 구현하며 Enter는 문을 잠그는 행위, Exit는 문을 여는 행위라 생각하면 된다. 

 

 

 

            Thread 1에서 Monitor.Enter(_obj)를 통해 임계 영역(Critical Section)에 들어가면, 

            Thread2는 Thread 1에서 Monitor.Exit(obj)로 해제할 때 까지 해당 영역에 접근하지 못한다. 

 

    *임계 영역 : 여러 스레드가 공유 자원에 접근할 때, 하나만 접근할 수 있도록 보장해 주는 영역. 

    static int number = 0;   //공유 자원 
    static object _obj = new object();   //좌물쇠 역할

    static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
        {
            /*상호배체 Mutual Exclusive */
            Monitor.Enter(_obj); //문을 잠그는 행위 -> 다른 이 들어올 수 없음 
            number++;
            Monitor.Exit(_obj);  //문을 다시 엶 -> 다른 이 접근 가능 
        }
    }

    static void Thread_2()
    {
        for (int i = 0; i < 100000; i++)
        {
            Monitor.Enter(_obj);
            number--;
            Monitor.Exit(_obj);
        }
    }

    static void Main(string[] args)
    {
        Task t1 = new Task(Thread_1);
        Task t2 = new Task(Thread_2);
        t1.Start();
        t2.Start();

        Task.WaitAll(t1, t2);

        Console.WriteLine(number);
    }
}

 

        만약, Moniter.Exit를 통해 문을 열지 않고 나가버리면 어떤 상황이 발생할까..

 

            👉 데드락(DeadLock)이 발생한다. 

 

 

 

 📌  DeadLock이란? 

            무한히 다음 자원을 기다리는 상태.

            상대방의 작업이 끝나기 만을 기다리고 있는 상태로 결과적으로 아무것도 완료되지 못하는 상태이다. 

 

 

    static int number = 0;   //공유 자원 
    static object _obj = new object();   //좌물쇠 역할

    static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
        {
            /*상호배체 Mutual Exclusive */
            Monitor.Enter(_obj); 
            {
                number++;
                return;       //문을 열지 않고 그냥 빠져나감 
            }
            Monitor.Exit(_obj);  
        }
    }

    //무한 대기 상태로 DeadLock이 발생함.
    static void Thread_2()
    {
        for (int i = 0; i < 100000; i++)
        {
            Monitor.Enter(_obj);
            number--;
            Monitor.Exit(_obj);
        } 
    }

 

        위와 같이 return을 해버리면 Exit로 빠져나오지 못하고 계속 잠겨있는 상태가 된다.

       그러면 Thread 2는 열리기만을 기다리는 상태로 무한 대기에 빠져 DeadLock이 발생한 것이다. 

 

 

 

   💡  해결 방법? 

            ▶  try finally로 해결할 수 있다. 

                  finally를 통해 해당 구문을 반드시 한번은 수행하기 때문이다. 

try
{
        Monitor.Enter(_lock);
        return;
}
finally
{
        Monitor.Exit(_lock);
}

       

         하지만 코드가 길어지고 복잡해 질 가능성이 있다..

        가장 좋은 방법은 lock을 사용하는 것이다. 

 

 

 

 

 📌  lock 이란? 

           크리티컬 섹션을 하나의 스레드만 실행할 수 있도록 해주는 것

 

 

            다음과 같이 구현하면 된다. 

static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
        {
            lock(_obj)
            {
                number++;
            }
        }
    }
static void Thread_2()
{
    for (int i = 0; i < 100000; i++)
    {
        lock(_obj)
        {
            number--;
        }
    } 
}

 

       안전한 스레드를 만들기 위해 Lock 방법을 사용하는 것을 가장 추천한다. 

 

 

 

 

출처 : https://www.inflearn.com/course/유니티-mmorpg-개발-part4

728x90
반응형
320x100
728x90

 

*  Interlocked란?

     int 형 값증가시키거나 감소시키는 데 사용하는 클래스 이다. 

     원자성순서를 보장해준다. (단점 : 성능에서 손해)

 

 

 

     다음과 같이 int 형 전역 변수인 number를 공유하고 있는 상황에서 

     Thread_1과 Thread_2가 number를 증가시키거나 감소시키는 작업을 하려 한다.

	static int number = 0;  //전역변수

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
                number++;
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
                number--;
        }

 

결과값으로 0을 기대 했지만, 이상한 값이 나오는 것을 볼 수 있다. 

 

 

 

왜..? 🤔

 

    thread1와 thread2가 동시에 number 값을 읽어와서 thread1이 number 값을 변경한다고 해도,

    thread2에서 다시 변경하면 thread1의 변경사항은 없어지기 때문이다. 

 

 

 

 

   number++와 number--는 코드 한 줄에 작동하는 것 처럼 보이지만,

   사실은 다음과 같이 세 단계를 통해 이루어 진다. 

 

int temp = number;
temp += 1;
number = temp;

 

 

 다시 말해, 자원을 가져다 쓰는 순서가 정해져 있지 않기 때문에 

 number = 0을 가지고 있는 상황에서 증감을 먼저 할지, 감산을 할지 알 수 없어 최종 값을

 보장할 수 없는 것이다. 

 

 

 

 

📌 해결 방법

 

       이를 해결하기 위해 Interlocked를 사용한다. 

 

     * Interlocked 클래스 (System.Threading 클래스 안에 있음) 

메소드 이름
설 명
CompareExchange 두 대상을 비교하여 값이 같으면 지정된 값을 설정하고, 그렇지 않으면 연산을 수행하지 않는다.
Decrement 지정된 변수의 값을 감소시키고 저장한다.
Exchange 변수를 지정된 값으로 설정한다.
Increment 지정된 변수의 값을 증가시키고 저장한다.

 

 

   이 때, 주의할 점이 값 그 자체를 전달하는 것이 아니라,

   ref를 통해 주소값을 참조하여 전달한다는 것이다. 

 

 

    그래야 다른 곳에서 이루어진 변경 작업에 대해 값을 업데이트 해 가져올 수 있다. 

 

 

 	static int number = 0;

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
                //ref로 넣어줌 -> 숫자 자체가 아니라 주소값을 넣어줌 
                Interlocked.Increment(ref number); 
                
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
                Interlocked.Decrement(ref number);  
        }
           
        static void Main(string[] args)
        {
            
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);
            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(number);
        }
    }

 

 

  이제 코드를 실행하면,

 

 

  올바른 값이 출력됨을 볼 수 있다. 

  Interlock정수 변수에 대해서만 사용한다는 것을 유의하자. 

 

 

 

 

출처 : https://www.inflearn.com/course/유니티-mmorpg-개발-part4

728x90
반응형
320x100
728x90

 

 

*  캐시(Cache)란?

          자주 사용하는 데이터나 값을 미리 복사해 둔 임시 장소이다. 

          저장공간이 작고 비용이 비싼 대신 빠른 성능 제공 

 

          장점 : 데이터를 미리 복사해 놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터 접근이 가능함.

 

 

          즉, DBMS 혹은 서버에 요청하기 보다는 메모리에 데이터를 저장하여 불러다 쓴다. 

              -> DBMS의 부하가 줄어들며 성능을 높이기 위해서 사용

 

 

 

 

*  캐시(Cache) 철학

       1) 시간 지역성(temporal locality) : 가장 최근에 사용한 변수가 또 사용될 확률이 높다

       2) 공간 지역성(spacial locality) : 방금 접근한 주소와 인접한 주소의 변수가 사용될 확률이 높다 

 

 

int[,] arr = new int[10000, 10000];
{
	long now = DateTime.Now.Ticks;
	for (int y = 0; y < 10000; y++)
		for (int x = 0; x < 10000; x++)
			arr[y, x] = 1;
	long end = DateTime.Now.Ticks;
	Console.WriteLine($"(y, x) 순서 걸린 시간{ end - now}");
}
{
	long now = DateTime.Now.Ticks;
	for (int y = 0; y < 10000; y++)
		for (int x = 0; x < 10000; x++)
			arr[x, y] = 1;
	long end = DateTime.Now.Ticks;
	Console.WriteLine($"(x, y) 순서 걸린 시간 {end - now }");   
}

 

 

 

 

 

실행 결과는 다음과 같다..

같은 기능을 수행하더라도 캐시의 공간 지역성으로 인해 차이가 나는 것이다. 

 

 

 

 

 


 

 

 

*  메모리 배리어(Memory Barrier)란?

         특정 연산의 순서를 강제하도록 하는 기능. CPU가 명령 순서를 지키도록 강제하여 원하는 결과를 얻도록 함. 

 

 

 

      사용 이유 ? 

 

          여러 스레드가 동시에 돌아가는 경우, 코드의 실행 순서가 바뀌는 경우가 발생할 수 있다. 

                  이 때, 다른 스레드에서 그 부분에 대한 메모리를 접근하여 잘못된 결과가 야기된다. 

 

 

     용도 


          -  코드 재배치 억제 : 컴파일러 최적화 시, 코드가 재배치 되는 것을 막는다 
          -  가시성 : 한 스레드에서 변경된 값이 다른 스레드에서 제대로 읽어지도록 함

 

 

 

  • Full Memory Barrier : Store/Load 둘 다 막는다
  • Store MeMory Barrier : Store만 막는다
  • Load Memory Barrier : Load만 막는다
       
	static int x = 0;
        static int y = 0;
        static int r1 = 0;
        static int r2 = 0;

        static void Thread_1()
        {
            y = 1;  //Store

            //---------------------------------------
            Thread.MemoryBarrier();
            //---------------------------------------


            r1 = x; //Load
        }
        static void Thread_2()
        {
            x = 1;
            
            //---------------------------------------
            Thread.MemoryBarrier();
            //---------------------------------------
            
            r2 = y;
        }

        int _answer;
        bool _complete;

        void A()
        {
            _answer = 123;
            Thread.MemoryBarrier();  // Barrier 1
            _complete = true;        
            Thread.MemoryBarrier();  //Barrier 2

        }

        void B()
        {
            Thread.MemoryBarrier();    //Barrier 3
            if (_complete)
            {
                Thread.MemoryBarrier(); // Barrier 4
                Console.WriteLine("_answer");
            }
        }

 

 

Barrier 1은 코드 재배치를 억제하면서 _answer 값을 없데이트함.

Barrier 2는 _complete을 업데이트 시켜주며

Barrier 3은 _complete을 확인하기 전에 읽어 값을 반영해주며

Barrier 4는 _answer 출력 전에 값을 업데이트 해주어 반영된 값이 올바르게 출력된다. 

 

 

출처 : https://www.inflearn.com/course/유니티-mmorpg-개발-part4

728x90
반응형
320x100
728x90

스레드에 이어서... 

 

다음과 같이 스레드를 작성하고

debug 모드로 실행하면 정상적으로 실행되는 것을 볼 수 있다. 

 

 

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

namespace ServerCore
{
    class Program
    {
        static bool _stop = false; //모든 스레드가 동시에 접근함 
        static void ThreadMain()
        {
            Console.WriteLine("쓰레드 시작!");  //단축키 : cw를 치고 tab 두번 누르기
            while(_stop == false)
            {
                //누군가가 stop 신호를 해주기를 기다림

            }
            Console.WriteLine("쓰레드 종료!");

        }
        static void Main(string[] args)
        {
            Task t = new Task(ThreadMain);
            t.Start();

            Thread.Sleep(1000); //1초 동안 대기 

            _stop = true;

            Console.WriteLine("Stop 호출");
            Console.WriteLine("종료 대기중");
            t.Wait(); //스레드가 종료 되었는 지 확인 (thread 사용 시에는 Join 사용, Task 사용 시에는 wait 사용)

            Console.WriteLine("종료 성공");
          

     
        }
    }
}

 

 

 

 

 

debug 모드를 release 모드로 변경하면 최적화가 진행된다. 

 

 

 

 

release 모드로 변경하고 다시 디버깅을 하면 

 

 

 

위와 같이 종료 대기중에 멈춰 있는 것을 볼 수 있다. 

Wait()에서 돌아오지 않고 스레드메인의 무한 루프를 돌고 있을 것이다...

 

 

 

 

이처럼 멀티 스레드를 만들 때, release 모드로 최적화 하면 debug에서는 되는 것이

안되는 경우가 발생할 수 있다. 

 

 

왜냐..

 

스레드 안의 while문을 다음과 같은 로직으로 받아들이기 때문이다. 

 

if(_stop == false)
{
	while (true)
	{
    
	}
}

 

 

 

이 때, volatile을 함께 선언해주면 최적화 되는 것을 막을 수 있다. 

 

다음과 같이 정상적으로 실행된다... 

(하지만 사용 추천하지 않음) 

 

 

 

 

 

 

                         출처 : https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part4

728x90
반응형
320x100
728x90

 

자료구조 분류

 

  • 단순 구조(Primitive Data Structure) : 기본적인 데이터 타입. 정수, 실수, 문자, 부울린 등의 기초 타입들
  • 선형 구조(Linear Data Structure) : 자료가 선형적으로 연결되어 있는 구조. 배열, 연결리스트, 스택, 큐
  • 비선형 구조(Non-linear Structure) : 자료 간 관계가 1대 다 혹은 다대다 구조.                                                                                                                             네트워크 혹은 계층구조를 가짐. 트리, 그래프
  • 파일 구조 : 레코드의 집합인 파일에 대한 자료구조. 순차파일, 색인 파일, 직접파일.. 

 

 

 


 

 

 

1. 배열이란?

          ▶  배열 : 연속적인 메모리 상에 동일한 데이터 타입의 요소들을 순차적으로 저장하는 자료구조

                              -   각 요소마다 인덱스를 갖고 있음

                              -  크기가 고정되어 있음 

 

 

1차원 배열 선언

Int[] A = new int[10];

 

 

2차원 배열 선언

Int[,] A = new int[3,4];

 

 

 

 

1.2 가변 배열(Jagged Array)이란?

          ▶  가변 배열(Jagged Array) : 배열 요소가 배열 타입인 경우.

                             일반 다차원의 배열의 경우 [,] 와 같이 콤마로 차원을 분리한다

                         하지만, 가변 배열[][]와 같이 괄호를 겹처서 차원을 표시한다.

 

 

            ▶    사용 경우

                  1. 일반 다차원 배열로 했을 때, 공간의 낭비가 심해지는 경우 

                  2. 각 차원마다 다른 배열 크기를 가져야 하는 경우에 사용

 

 

//가변 배열 선언
int[][] A = new int[3][];

//각 1차원 배열 요소당 서로 다른 크기의 배열 할당
A[0] = new int[2];
A[1] = new int[6]{1,2,3,4,5,6};
A[2] = new int[3]{9,8,7};

 

 

 

 

 

1.3 동적 배열(Dynamic Array)이란? 

          ▶  정적 배열(Static Array) : 초기화시 크기가 미리 정해지며, 처음 지정한 고정 크기를 그대로 계속 유지함

                                                              ex) int[], strring[,] 과 같은 식으로 선언된 배열은 모두 정적 배열임

 

          ▶  동적 배열(Dynamic Array) : 배열의 최대 크기를 미리 알 수 없을 때,                                                                                                                                      배열을 중간에 확장 혹은 축소 해야 할 때 사용하면 용이하다. 

 

 

 

          동적 배열 만드는 방법 : 

                 1. 기존 배열 크기보가 1개 더 큰 임시 배열 생성

                 2. 임시 배열에 기존 배열값 복사

                 3. 기존 배열에 임시 배열 할당

                 4. 기존 배열의 마지막 요소에 새 배열 요소 추가 

 

    public object[] arr = new object[0];
    public void Add(object elemnet)
    {
        var temp = new object[arr.Length + 1]; //임시 배열 생성
        for(int i=0; i<arr.Length; i++)
        {
            temp[i] = arr[i]; //기존 배열 요소를 하나씩 복사함
        }
        arr = temp;  //기존 배열에 임시 배열 할당 

        arr[arr.Length - 1] = elemnet; //마지막 요소에 새 요소 할당 
    }

 

             단점 : 필요할 때마다 매번 새로운 배열 공간을 생성하고 기존 배열요소를 전부 복사해야 하는 번거로움이 있음 

                       O(n) 시간 복잡도 

 

 

           * 해결 방법 : 배열을 2배씩 확장한다. 

           Capacity : 배열의 최대 수용가능 용량

           Count : 현재 배열요소  

           count가 capacity 보다 크거나 같으면(배열이 꽉차면)  2배로 확장하고 새 배열 요소를 추가한다. 

    private object[] arr2;
    private const int GROWTH_FACTOR = 2;

    public int Count { get; private set; }
    public int Capacity
    {
        get { return arr2.Length; }
    }

    public void DynamicArray(int capacity = 16)
    {
        arr2 = new object[capacity];
        Count = 0;
    }

    public void AddArray(object element)
    {
        //배열이 꽉 찼을 때 확장
        if(Count >= Capacity)
        {
            int newSize = Capacity * GROWTH_FACTOR;
            var temp = new object[newSize];
            for(int i=0; i<arr2.Length; i++)
            {
                temp[i] = arr2[i];
            }
            arr2 = temp;
        }
        arr2[Count] = element;
        Count++;
    }

    public object Get(int index)
    {
        if(index > Capacity - 1)
        {
            throw new ApplicationException("Element not found");
        }
        return arr2[index];
    }

 

 

 

 

 

 

1.4 원형 배열(Circular Array) 이란? 

        ▶   원형 배열(Circular Array) : 고정된 크기의 배열을 양 끝이 연결된 것처럼 사용 가능 

                         배열의 크기가 N 일 때, 배열의 마지막 요소(N-1)에 도착하면, 다음 배열요소는 첫번째 요소(0)가 된다. 

 

                            - 원형 배열 : 선입선출(FIFO), 큐를 구현하거나 데이터 스트림 버퍼를 구현할 때 사용  

                            - 선형 배열 : 선입후출(LIFO)

 

 

                원형 배열 순찰을 위해서 mod 연산자를 사용하여 마지막 배열의 다음 인덱스가 첫 배열 인덱스로 돌아오게 한다. 

    char[] A = "abcdefgh".ToCharArray();
    int startIndex = 2;

    for(int i=0; i<A.Length; i++)
    {
        int index = (startIndex + i) % A.Length;
        Console.WriteLine(index);
    }

 

 

 

 

 

 

 

1.5 .NET 배열 클래스 

  .NET Framework에서 지원하는 동적 배열은 ArrayListList<T>가 있다. 

 

 

      ▶   ArrayList 

ArrayList ArrList = new ArrayList();

ArrList.Add("A");        // index 0
ArrList.Add("B");        // index 1
ArrList.Add("C");        // index 2

ArrList.RemoveAt(1);    

ArrList.Add("D");        // Add는 맨 끝에 추가하니깐 index 2 에 삽입됨

ArrList.Insert(2, "E");    //index 2의 자리를 E로 대체

 

 

     ▶    List<T>

                List<T>는 임의의 타입(T)을 지정할 수 있는 제네릭 동적 배열이다.

                   ex) List<int>, List<string>..

                지정된 타입만을 담을 수 있으며, 다양한 타입을 담을 때는 ArrayList가 더 유용하다.

    List<int> a = new List<int>();
    for(int i=1; i<=17; i++)
    {
        a.Add(i);
        Console.WriteLine("{0} : {1}", i, a.Capacity);
    }

         capacity는 2배씩 증가한다. 

 

 

 

 

 

출처 : C#으로 이해하는 자료구조

728x90
반응형

'게임 서버 > C#' 카테고리의 다른 글

[c#/디자인패턴] - 싱글톤(Singleton) 패턴  (3) 2022.12.20

이것 저것..

얘리밍
|2022. 12. 6. 03:37
320x100
728x90

 

 

안드로이드 하다

웹 개발 하다

 

플러터 앱 만들다, 웹 유지보수 하다가

 

이제는 게임 서버를 하게 됐다

 

이것 저것 시도만 하는 것 처럼 보일 수 있지만..

 

나 스스로 경계와 한계를 짓지 않고 

지금 맡은 것에 최선을 다하기로 했다..

 

 

배우고 싶은 것도 많고 알고 싶은 것도 많기에 

꾸준히 다양한 분야에 대해 공부할 것이다.

 

언어만 다를 뿐, 결국 다 컴퓨터와 소통하는 방법이다.

 

 

 

요즘 관심이 가장 많은 것은 IOS 개발!

애플 아카데미에도 지원했다

 

후에 좋은 결과가 있길...😊

728x90
반응형

'하루 일기' 카테고리의 다른 글

새로운 시작👏  (3) 2023.01.25
취뽀했어용  (2) 2022.04.06
코테 연습하기  (3) 2022.03.17
320x100
728x90

 

 

 

 

프로세스와 스레드에 대해 먼저 알아보자.

 

 

프로세스 : 현재 실행 중인 프로그램

스레드 : 운영 체제가 프로세서 시간을 할당하는 기본 단위 / 명령어를 실행하기 위한 스케쥴링 단위 

 

 

쉽게 말해서, 스레드는 작업자라고 생각하면 된다. 

어떠한 업무가 주어졌을 때, 그 일을 수행하는 작업자가 바로 스레드 이다.

 

 

 

 


 

 

 

 

* NameSpace(네임스페이스)  선언하기 

using System.Threading;

스레드를 사용하기 위해서 다음과 같이 선언해주자

 

 

1) 스레드 선언하기 

Thread t = new Thread(MainTread);

스레드를 사용하기 위해 다음과 같이 스레드 생성자를 만든다. 

넘겨주는 MainTread 함수 또한 생성한다. 

static void MainTread(object state)
 {
 	Console.WriteLine("Hello Thread!");     
 }

 

 

 

기본 스레드foreground에서 실행되는 형태이다.

스레드의 이름을 설정하고 싶으면 Name 을 활용한다.

 

static void Main(string[] args)
{
	Thread t = new Thread(MainTread);
    
	t.Name = "Test Thread"; //이름을 설정한다.
    	t.Start(); //스레드 실행
    
    
    	Console.WriteLine("Waiting for Thread!"); //메인 작업
}

 

 

 

 

이 때, 다음과 같이 스레드를 무한 반복으로 실행하면

메인 종료와 상관없이 계속해서 실행된다..

(foreground 스레드는 부모 스레드가 종료되어도 함께 종료되지 않고 계속 동작한다 )

 

static void MainTread()
{
        //메인 함수와 상관없이 지속해서 실행됨(영영 실행..)  
 	while(true) 
		Console.WriteLine("Hello Thread!");

 

 

계속해서 실행되는 스레드

 

 

 

이를 백그라운드에서 실행하면, 스레드는 실행 여부와 상관없이 메인 함수가 종료되면 같이 종료된다. 

t.IsBackground = true;   //기본 값은 false이다.
t.Start();

t.Join();   //스레드가 끝날 때 까지 기다림

 

join은 스레드가 종료될 때 까지를 기다린다. 즉, 스레드가 종료되었는 지를 확인하고 작업을 종료한다.

 

 

 

 

 

 

 

2) 스레드 풀(ThreadPool) 사용하기 

 

 

thread 와 threadPool의 차이에 대해 먼저 알아보면, 

    thread

  • 스레드를 사용할 때는 개수를 제한하지를 않는다. 
  • 하지만 스레드 수와 cpu core 수를 맞춰주는 것이 좋다 

   

    threadPool

  • 스레드 풀을 사용하면  최대로 돌릴 수 있는 스레드를 제한한다.
  • 아주 많은 개수를 실행시켜도 기존의 프로세스를 수행한 다음, 이후에 일을 수행한다. 

 

 

ThreadPool은 단기로 실행하는 것에 대해 유용하다. 

static void MainTread(object state)
 	{
       
            for (int i = 0; i < 5; i++)
                Console.WriteLine("Hello Thread!");
        }

...

static void Main(string[] args)
    {
    		ThreadPool.SetMinThreads(1, 1); //최소 스레드 개수
            	ThreadPool.SetMaxThreads(5, 5); //최대 스레드 개수
            
    		ThreadPool.QueueUserWorkItem(MainTread);

 

thread와는 다르게 object 인자를 명시해 주어야 하며 스레드의 개수를 제한하므로

setMinThread setMaxThread를 통해 최소, 최대 스레드를 설정해 주면 된다. 

 

 

 

스레드 풀을 사용하여

최대 치 만큼 일을 할당하면 

다음과 같이 이후의 일을 하지 못하는 것을 볼 수 있다.

 

 

하지만 다음과 같이 네 개의 스레드만 할당하여 남는 것이 있다면, 

나머지 작업을 원활하게 수행 가능하다.

for (int i = 0; i < 4; i++)
	ThreadPool.QueueUserWorkItem((obj) => { while (true) { } });

 

 

 

 

이처럼 모든 스레드가 할당 되었을 때, 일이 막히는 상황을 해결하는 방법은 무엇일까

Task를 사용하면 된다. 

 

 

 

 

 

 

 

3) Task 사용하기 

 

for(int i=0; i<5; i++)
        {
            //아주 오래걸리는 작업에 대해 
            Task t = new Task(() => { 
            	while (true) { } }, TaskCreationOptions.LongRunning);
            t.Start();
        }

 

TaskCreationOPtions.LongRunning 옵션을 설정해주면, 긴 작업이 수행되는 작업에 대해서도

일을 원활하게 수행 가능하다. 

 

이렇게 ThreadPool의 부족현상을 해결할 수 있다. 

 

 

 

 

 

 

 

 

 

 

 

출처 및 참고 : https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part4

728x90
반응형