SpinLock의 단점
 - Lock해제 되었는지 체크하기 위해선 매 프레임마다 Loop를 돌기때문에 비효율 적이다.

RandomLock
 - Lock이 해제됐는지 일정 "시간 주기"로 체크한다
 - 매 프레임마다 하지않기때문에 좀 더 효율적이다.
 - 어떠한 형태로 "시간 주기"를 설정하냐에 따라 동작이 다르다.

무조건 대기
 - 무조건 1ms 정도 대기한다.
 - 1ms "정도"인 이유는 정확한 대기시간은 운영체제의 스케쥴러가 결정한다.

Thread.Sleep(1)

조건부 대기
 - 자신보다 우선순위가 낮은 Thread한테는 양보 불가.
 - 자신보다 우선순위가 높거나 같은 Thread에게만 프로세스를 양보한다.
 - 이 경우 우선순위 설정에 따라 어떤 Thread는 기아현상(계속 프로세스 할당을 못받는 상황)을 겪게 될 수 있다.

Thread.Sleep(0)

관대한 대기
 - 지금 실행 가능한 Thread가 있으면 해당 Thread를 우선 실행시키고, 없을 경우에만 자신을 실행 시킨다.

Thread.Yield()

어떤 방법이 베스트라고 할 수 없고 상황에 맞게 사용하면 된다.

'프로그래밍 > 네트워크' 카테고리의 다른 글

SpinLock  (0) 2021.09.01
Lock  (0) 2021.08.29
Race Condition  (0) 2021.08.29
Cache  (0) 2021.08.27
Thread의 생성  (0) 2021.08.27

1. SpinLock : 존버메타
 - Lock풀릴때까지 무한정 기다린다.

느낌적으로 짜보자.

   class SpinLock
    {
        volatile bool _locked = false;

        public void Acquire()
        {
            // 잠금이 풀릴때 까지 대기
            while (_locked)
            {
            }

            // 획득
            _locked = true;
        }

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

    class Program
    {
        const int MAX = 100000;
        static int _num = 0;
        static SpinLock _lock = new SpinLock();

        static void Thread_1()
        {
            for (int i = 0; i < MAX; ++i)
            {
                _lock.Acquire(); // 획득
                ++_num;
                _lock.Release(); // 해제
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < MAX; ++i)
            {
                _lock.Acquire(); // 획득
                --_num;
                _lock.Release(); // 해제
            }
        }

        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(_num);
        }
    }

결과
 - 어림없다.
 - Acquire 함수가 Atomic이 보장이 안되기때문.

        public void Acquire()
        {
            // 두개 Thread가 동시에 접근할 경우 _locked는 아직 false이므로
            while (_locked)
            {
            }

            // 두 Thread 모두 _locked를 획득하게 된다.
            _locked = true;
        }

해결법 1.
Interlocked.Exchange 사용
 - Atomic을 보장하는 함수
 - 값을 ref 변수에 Atomic을 보장하여 넣어준다.
 - 반환값은 값을 넣기전에 값

    class SpinLock
    {
        volatile int _locked = 0;

        public void Acquire()
        {
            while (true)
            {
                // Interlocked으로 _locked에 1을 대입한다.
                // 반환값이 0이면 이전에 사용하고 있던 Thread가 없는 것이고,
                // 반환값이 1이면 이전에 Thread가 사용하고 있고 아직 Release안된 상태.
                int original = Interlocked.Exchange(ref _locked, 1);
                if (original == 0)
                    break;
            }
        }

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

해결법 2.
위 방법도 괜찮지만 뭔가 직관적이지 않음
 - 무조건 Lock을 걸어주고, Lock걸기 이전에 Lock이 안걸려있는 경우에 Lock이 걸린다 : 복잡..
 - Lock이 걸려있지 않으면 Lock을 걸어준다 : Good
 - 간단한 코드로 써보면

if(_locked == 0)
    _locked = 1;

하지만 이렇게하면 Atomic하지 않기 때문에 Interlocked에서 제공되는 함수가 있다.
Interlocked.CompareExchange(ref _locked, 넣을값, 비교값)
 - _locked와 '비교값'을 비교하여 같으면 '넣을값'을 _locked에 넣어준다.
 - Interlocked.Exchange와 똑같이 original값을 반환한다.

int original = Interlocked.CompareExchange(ref _locked, 1, 0);
if (original == 0)
    break;

변수를 사용하여 좀 더 명시적으로 바꾸면

int expected = 0; // 예상한 값
int desired = 1; // 예상한 값이 맞으면 넣을 값
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
    break;

전체코드

    class SpinLock
    {
        volatile int _locked = 0;

        public void Acquire()
        {
            while (true)
            {
                int expected = 0; // 예상한 값
                int desired = 1; // 예상한 값이 맞으면 넣을 값

                // _locked가 0이면 1을 넣어주고, 0(original == expected)을 반환다.
                // original이 0이었다는건 이전에 사용하고있지 않은 상태이기 때문에 사용권을 획득한다.
                if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
    				break;
            }
        }

        public void Release()
        {
            // 이미 Acquire 단계에서 Atomic이 보장된 상황이기때문에 별도 Interlocked없이 해제해도 된다.
            _locked = 0;
        }
    }

'프로그래밍 > 네트워크' 카테고리의 다른 글

Spin Lock 개선  (0) 2021.09.02
Lock  (0) 2021.08.29
Race Condition  (0) 2021.08.29
Cache  (0) 2021.08.27
Thread의 생성  (0) 2021.08.27

이전예제에서 봤던 DeadLock상황은 엄청 단순한 상황.
실제론 좀 더 복잡한 상황에서 자주 발생함.

2개 이상의 lock이 존재할 경우.
 - Thread1이 Resource1을 Lock걸어 놓고 Resource2를 접근하려고 했지만. Resource2는 이미 Thread2에 의해 Lock걸린 상황
 - 그와 동시에 Thread2는 Thread1에 의해 lock이 걸린 Resource1에 접근하려고 하면 Thread1과 Thread2는 둘다 "교착상태"가 발생하여 더이상 프로세스가 진행되지 않는다.

예시.

해결책.
1. Lock에 접근했을때 일정 시간동안 기다리다가 Lock이 해제가 안되면 포기한다.
 - 해결을 될지 몰라도 근본적인 Lock구조가 꼬여있는 상태는 해결하지 못하기 때문에 좋은 방법은 아니다.
2. DeadLock이 발생했을때 디버깅을 통해 Lock구조를 수정하여 해결
 - 근본적인 해결이지만  DeadLock상황이 개발단계에서 발생하기 힘들기 때문에 문제 발견후 수정이 꽤나 어려움
3. Lock별로 Id를 부여하여 Lock의 순서도를 가식화 하여 관리
 - 2번을 미리 알기위한 방법중 하나

그외 여러방법이 있을 수 있지만 근본적으론 DeadLock을 발견되면 즉시 Lock구조를 수정하는 방법이 최선이고 경험을 통해 구조를 잘짜는법 뿐인것같다.

Interlocked는 정수만 제어가능하다는 단점이 있다.
 - 구문을 Atomic하게 제어할 수 있는 무언가가 필요하다.

Monitor를 이용한 Atomic구현
 - 빈 object를 기준으로 Atomic영역의 시작과 끝을 정의할 수 있음.

하지만 코드가 길어질 경우 실수로 return을 하여 Monitor.Exit가 실행이 안되면 프로그램이 멈춰버림
 - Thread_1은 return이후에 정상적으로 실행이 되겠지만, Thread_2의 경우 Thread_1에 의해 잠겨버린 _obj에 의해 동작이 멈춰버리게 된다.
 - Thread_2는  Dead Lock 상태(죽은 자물쇠)
 - 문을 잠그고 안열어준 상태

해결책1.
try - finally
 - 구문을 try로 묶고 finally에 Lock해제구문을 넣게되면 중간에 return을 하더래도 무조건 finally가 실행되어 Lock해제 된다.

해결책2.
"lock"키워드 사용
 - lock내부적으론 위와 같이 Monitor.Enter와 Monitor.Exit로 구현돼 있다.
 - Monitor를 사용하는 것보다 좀더 편리하고 안전하게 Atomic영역을 관리할 수 있다.

'프로그래밍 > 네트워크' 카테고리의 다른 글

Spin Lock 개선  (0) 2021.09.02
SpinLock  (0) 2021.09.01
Race Condition  (0) 2021.08.29
Cache  (0) 2021.08.27
Thread의 생성  (0) 2021.08.27

MultiThread 동작 시 하나의 변수에 대해 2개의 Thread가 값을 변경할 경우 예상과 다른 결과가 나올 수 있다.
 - 값 연산 횟수가 많아질수록 더 심해진다.

 

이유
 - Assembly Level에서의 변수의 값증가 행위는 3단계로 나뉜다.
  1. 해당 변수의 주소에 접근하여 값을 ecx register에 임시 저장한다.
  2. ecx를 1증가 시킨다.
  3. ecx값을 변수 주소에 다시 넣어준다.
- Multi Thread에 경우 Atomic(원자성)이 보존되지 않아 1번 동작이 2개의 Thread에서 동시에 일어나게 되면 서로 다른 연산을 하게된다.
 - 이 경우 "변수의 주소"는 Critical Section(임계 영역)이라고 한다.

// c#에서 증가
number++;

// Assembly에서 증가
mov exc, dword ptr[주소] // number주소에서 exc로 number값 저장
inc exc // exc(number에서 가져온값)을 1증가
mov dword ptr[주소], exc // exc의 증가된값을 number 주소로 다시 넣어준다.

// Assembly를 C#느낌으로 쉽게 풀어쓰면
int temp = number;
temp++;
number = temp;

 Interlocked
- Atomic을 보장하는 기능
- 2개의 Thread가 하나의 주소에 접근할 경우 둘 중 먼저 접근한 Thread의 연산처리가 끝난뒤 다른 Thread가 순차적으로 접근하여 연산하게 된다.
- 때문에 성능상의 저하는 피할 수 없다.

MultiThread환경에서 변수의 증감에 대한 Atomic한 결과를 확인하는 방법

// 잘못된 방법
Interlocked.Increment(ref number);
int result = number; // 이 시점에 number가 위에서 증가된 이후에 실행됐다는 보장이 없다.

// 옳바른 방법
// Interlocked.Increment의 반환값은 증가된 순간의 값을 반환하므로 Atomic한 값이다.
int result = Interlocked.Increment(ref number);

'프로그래밍 > 네트워크' 카테고리의 다른 글

SpinLock  (0) 2021.09.01
Lock  (0) 2021.08.29
Cache  (0) 2021.08.27
Thread의 생성  (0) 2021.08.27
Thread란?  (0) 2021.08.26

Cache
 - CPU가 메모리에 데이터를 전송하기 전 데이터를 저장하고있는 저장 공간
 - 데이터를 바로 RAM에 올리는것이 아닌, 일정량을 Cache에 저장하고 있다가 한번에 RAM에 올린다.
 - 속도는 Register -> L1 Cache -> L2 Cache -> RAM(MainMemory) 순이다.

Cache 철학 - 무엇을 Cache 할것인가?
 - 시간적 측면 : 가장 최근에 사용한 데이터
 - 공간적 측면 : 가장 최근에 사용된 메모리 근처

Cache Test
 - 2차원 배열에서 [0,0][0,1][0,2]를 참조하는 것과 [0,0][1,0][2,0]을 참조하는 시간이 차이가 난다.
 - 공간적 측면에서 보면 [0,0]을 참조한 순간 Cache는 [0,?]을 우선으로 사용할 것이라고 가정했기 때문.

'프로그래밍 > 네트워크' 카테고리의 다른 글

SpinLock  (0) 2021.09.01
Lock  (0) 2021.08.29
Race Condition  (0) 2021.08.29
Thread의 생성  (0) 2021.08.27
Thread란?  (0) 2021.08.26

Thread의 기본 생성
 - Thread 생성시 넣은 함수가 종료되면 Thread도 같이 종료된다.

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

static void Main(string[] args)
{
    // Thread 생성 (생성된 Thread가 동작시킬 함수)
    Thread t = new Thread(MainTread);
    // Thread 시작
    t.Start();

    Console.WriteLine("Hello world");
}

C#의 Thread는 Default로 Foreground로 생성된다.
 - 이 경우 Thread가 종료되지 않으면 프로그램이 종료되지 않는다.
 - isBackground = true 해줄 경우 Thread가 background로 실행되기 때문에 프로그램 종료와 함께 종료된다.

        static void MainTread()
        {
            while(true)
                Console.WriteLine("Hello Thread");
        }

        static void Main(string[] args)
        {
            // Thread 생성 (생성된 Thread가 동작시킬 함수)
            Thread t = new Thread(MainTread);
            t.IsBackground = true;
            // Thread 시작
            t.Start();

            Console.WriteLine("Hello world");
        }

생성한 Thread가 종료되기전까지 프로그램이 종료되지 않는다. / isBackground = true일 경우 프로그램이 종료되면 Thread도 종료된다.

Thread가 Background로 실행되고 있어도 Join()함수를 통해 해당 Thread가 종료될때까지 프로그램을 대기시킬 수 있다.

        static void MainTread()
        {
            while(true)
                Console.WriteLine("Hello Thread");
        }

        static void Main(string[] args)
        {
            // Thread 생성 (생성된 Thread가 동작시킬 함수)
            Thread t = new Thread(MainTread);
            t.IsBackground = true;
            // Thread 시작
            t.Start();
            Console.WriteLine("Waiting for Thread");
            // background Thread가 종료될때 까지 대기
            t.Join();

            Console.WriteLine("Hello world");
        }

디버깅을 통해 현재 Thread상태 확인하기
 - 프로그램 실행중 "정지"버튼을 누르고 Thread를 선택하면 해당 Thread의 동작지점이 표시된다.

Thread Pool
 - 프로그램 동작 중 간단한 작업을 위해 잦은 Thread 생성이 필요한 경우 사용하면 좋음.
 - C#에서 미리 Poolling해둔 Thread를 가져다가 사용하고 해당 Thread가 종료되면 자동으로 Pool로 반납됨
 - 대신 ThreadPool에서 가져온 Thread를 오래 사용할 경우 ThreadPool로 반납이 안되어, ThreadPool의 Thread가 부족해질 수 있고, 그럴경우 ThreadPool이 동작하지 않는 상황 발생.
 - 그래서 간단한 작업을 할때 사용하자.

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

        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(MainTread);

            while (true)
            {

            }
        }

Thread Pool의 Max개수만큼 Thread를 사용하고 반납이 안될경우 더 이상 추가 Thread할당이 안됨

Task
 - TheadPool에서 Thread를 할당 받아 사용.
 - 하지만 Task생성시 옵션으로 "TaskCreationOptions.LongRunning"을 설정해 줄 경우 ThreadPool에서 할당하지 않고 별도의 Thread를 생성하여 할당하기 때문에 Thread Pool의 부족현상을 해결 할 수 있다.
 - "TaskCreationOptions.LongRunning" 옵션을 넣지 않으면 ThreadPool에서 할당한다.
 - 따라서 Task위주로 Thread를 활용하고 오래걸리는 작업일 경우 Task생성시 "TaskCreationOptions.LongRunning"를 넣어주자.

'프로그래밍 > 네트워크' 카테고리의 다른 글

SpinLock  (0) 2021.09.01
Lock  (0) 2021.08.29
Race Condition  (0) 2021.08.29
Cache  (0) 2021.08.27
Thread란?  (0) 2021.08.26

CPU가 Single Core일 때 Thread 할당 방법
 - Core의 작업을 나누어 각 Thread별로 일정 단위시간만큼 할당한다.

CPU가 Multi Core일 때 Thread 할당 방법
 - 각 코어가 Thread별로 할당됨
 - Thread를 많이 생성한다고 효율이 좋지않다.
  -> Thread가 많을 수록 CPU가 Thread를 옮겨가며 작업하는 횟수가 많아지고 그 행위자체가 비효율
 - 그래서 Thread숫자는 Core숫자와 맞춰주는게 효율이 좋다.

Multi Thread의 메모리 영역
 - Heap : 공용
 - 데이터(static) : 공용
 - stack : 쓰레드별 할당

Thread가 많아질 수록 관리해야 될 부분이 많아져 혼란해진다.

Multi Thread 활용시 Heap과 데이터 영역에 접근할때 이슈가 발생할 수 있다.

'프로그래밍 > 네트워크' 카테고리의 다른 글

SpinLock  (0) 2021.09.01
Lock  (0) 2021.08.29
Race Condition  (0) 2021.08.29
Cache  (0) 2021.08.27
Thread의 생성  (0) 2021.08.27

+ Recent posts