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

namespace ServerCore
{
    // 재귀적 락 허용 X
    // 스핀락 => 5000번 시도 후 Yield
    class Lock
    {
        const int EMPTY_FLAG = 0x00000000;
        const int WRITE_MASK = 0x7FFF0000;
        const int READ_MASK = 0x0000FFFF;
        const int MAX_SPIN_COUNT = 5000;

        // [Unused(1)] [WriteThreadid(15)] [ReadCount(16)]
        int _flag = EMPTY_FLAG;

        // 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 경합해서 소유권을 얻는다.
        public void WriteLock()
        {
            int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
            while(true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG) return;
                }
                Thread.Yield();
            }
        }
        
        public void WriteUnlock()
        {
            Interlocked.Exchange(ref _flag, EMPTY_FLAG);
        }
        // 아무도 WriteLock을 획득하고 있지 않으면 ReadCount를 1 늘린다.
        public void ReadLock()
        {
            while (true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    int expected = (_flag & READ_MASK);

                    if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected) return;
                }
                Thread.Yield();
            }
        }

        public void ReadUnlock()
        {
            Interlocked.Decrement(ref _flag);
        }
    }
}

재귀적 락을 허용하지 않는 ReaderWriterLock 구현
 

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

namespace ServerCore
{
    // 재귀적 락 허용 => WriteLock -> WriteLock Ok, WriterLock -> ReadLock Ok, ReadLock -> WriteLock No
    // 스핀락 => 5000번 시도 후 Yield
    class Lock
    {
        const int EMPTY_FLAG = 0x00000000;
        const int WRITE_MASK = 0x7FFF0000;
        const int READ_MASK = 0x0000FFFF;
        const int MAX_SPIN_COUNT = 5000;

        // [Unused(1)] [WriteThreadid(15)] [ReadCount(16)]
        int _flag = EMPTY_FLAG;
        int _writeCount = 0;

        public void WriteLock()
        {
            // 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
            int lockThreadId = (_flag & WRITE_MASK) >> 16;
            if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
            {
                _writeCount++;
                return;
            }

            // 아무도 WriteLock or ReadLock을 획득하고 있지 않을 때, 경합해서 소유권을 얻는다.
            int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK;
            while(true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
                    {
                        _writeCount = 1;
                        return;
                    }
                }
                Thread.Yield();
            }
        }
        
        public void WriteUnlock()
        {
            int lockCount = --_writeCount;
            if (lockCount == 0) Interlocked.Exchange(ref _flag, EMPTY_FLAG);
        }
        
        public void ReadLock()
        {
            // 동일 쓰레드가 WriteLock을 이미 획득하고 있는지 확인
            int lockThreadId = (_flag & WRITE_MASK) >> 16;
            if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
            {
                Interlocked.Increment(ref _flag);
                return;
            }

            // 아무도 WriteLock을 획득하고 있지 않으면 ReadCount를 1 늘린다.
            while (true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    int expected = (_flag & READ_MASK);

                    if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected) return;
                }
                Thread.Yield();
            }
        }

        public void ReadUnlock()
        {
            Interlocked.Decrement(ref _flag);
        }
    }
}

재귀적 락을 허용하는 ReaderWriterLock 구현
W -> R순으로 Lock을 하였다면 R -> W 순으로 Unlock을 해야 함
 

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

namespace ServerCore
{

    class Program
    {
        static volatile int count = 0;
        static Lock _lock = new Lock();

        static void Main(string[] args)
        {
            Task t1 = new Task(delegate ()
            {
                for (int i = 0; i < 100000; i++)
                {
                    _lock.WriteLock();
                    count++;
                    _lock.WriteUnlock();
                }
            });

            Task t2 = new Task(delegate ()
            {
                for (int i = 0; i < 100000; i++)
                {
                    _lock.WriteLock();
                    count--;
                    _lock.WriteUnlock();
                }
            });

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(count);
        }
    }
}

ReaderWriterLock의 사용

'네트워크' 카테고리의 다른 글

소켓 프로그래밍 - Iterative 에코 서버  (0) 2023.09.10
소켓 프로그래밍 - TCP 소켓  (0) 2023.09.10
ReaderWriterLock  (0) 2023.08.04
AutoResetEvent  (0) 2023.08.03
SpinLock  (0) 2023.08.02
class Reward
{
    static ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

    // id를 조회하여 그에 맞는 리워드를 주는 함수
    static Reward GetRewardByid (int id)
    {
        lock (_lock)
        {

        }
        return null;
    }
    // 리워드를 추가하는 함수, 자주 사용 안하는 함수   
    static void AddReward (Reward reward)
    {
        lock(_lock)
        {

        }
    }
}

게임에서 리워드가 있고 이를 받는 경우와 리워드를 추가하는 경우가 있다면 받는 경우가 대부분이며, 추가하는 경우는 극히 드물다.
이 경우 둘다 Lock을 거는것은 효율성의 문제가 된다.
 

class Reward
{
    static ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

    // id를 조회하여 그에 맞는 리워드를 주는 함수
    static Reward GetRewardByid (int id)
    {
        _lock.EnterReadLock();
        _lock.ExitReadLock();
        return null;
    }
    // 리워드를 추가하는 함수, 자주 사용 안하는 함수   
    static void AddReward (Reward reward)
    {
        _lock.EnterWriteLock();
        _lock.ExitWriteLock();
    }
}

이 경우 ReaderWriterLockSlim을 사용
 
WriteLock이 걸리지 않은 상태에서는 Read가 자유롭게 가능하지만 WriteLock이 걸린 상태라면 Read에도 Lock이 걸린다.

'네트워크' 카테고리의 다른 글

소켓 프로그래밍 - TCP 소켓  (0) 2023.09.10
ReaderWriterLock 구현  (0) 2023.08.05
AutoResetEvent  (0) 2023.08.03
SpinLock  (0) 2023.08.02
DeadLock  (0) 2023.08.01
class Lock
    {
        // ture = Lock이 풀려있는 상태
        AutoResetEvent _available = new AutoResetEvent(true);
        public void Acquire()
        {
            // Lock 시도 and 자동으로 false상태로 전환
            _available.WaitOne();
        }

            
        public void Release()
        {
            // Event의 상태를 signal상태로 바꿈 = true로 전환
            // _available.Reset() = false로 전환
            _available.Set();
        }
    }

Event를 사용하여 Lock이 풀리면 알려주는 방법
 

static void Thread1()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num++;
                _lock.Release();
            }
        }

        static void Thread2()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num--;
                _lock.Release();
            }
        }

단, 시간이 조금 걸려 이전처럼 10만번을 돌리면 바로 결과가 뜨지 않는다.
 

ManualResetEvent _available = new ManualResetEvent(true);
        public void Acquire()
        {
            _available.WaitOne();
            _available.Reset();
        }

AutoResetEvent의 WaitOne()에는 Reset이 포함되어있어서 자동으로 false가 된다.
ManualResetEvent를 사용할 경우 자동으로 false 전환이 되지 않아 Reset을 해주어야하는데 이럴 경우 true인지 확인하고 false로 만드는 행위가 2단계로 실행되어 원자적이지 않아 제대로 된 결과가 나오지 않는다.
(쓰레드를 여러개 입장 시키려는 경우 사용)
 
Event의 경우 커널까지 가기 때문에 속도가 느림
 

class Program
    {
        static int _num = 0;
        static Mutex _lock = new Mutex();
        
        static void Thread1()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.WaitOne();
                _num++;
                _lock.ReleaseMutex();
            }
        }

        static void Thread2()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.WaitOne();
                _num--;
                _lock.ReleaseMutex();
            }
        }

 
Mutex는 Event와 비슷하나 몇번 잠구었는지를 기억한다.
WaitOne()을 2번 호출했다면 ReleaseMutex()를 2번 호출해야 Lock이 풀린다.
또한 ThreadId를 가지고 있다.
WaitOne()을 한 Thread가 아닌 다른 Thread가 Release를 하려고 하면 Error

'네트워크' 카테고리의 다른 글

ReaderWriterLock 구현  (0) 2023.08.05
ReaderWriterLock  (0) 2023.08.04
SpinLock  (0) 2023.08.02
DeadLock  (0) 2023.08.01
Lock 기초  (0) 2023.07.31

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

namespace ServerCore
{
    class SpinLock
    {
        // _locked == true -> 잠김상태
        volatile bool _locked = false;
        public void Acquire()
        {
            // 잠김상태이면 무한루프로 풀리기를 기다린다.
            while(_locked)
            {
            }
            // 잠김이 풀렸으니 내가 사용
            _locked = true;
        }
        public void Release()
        {
            _locked = false;
        }
    }
    class Program
    {
        static int _num = 0;
        static SpinLock _lock = new SpinLock();
        
        static void Thread1()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num++;
                _lock.Release();
            }
        }

        static void Thread2()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num--;
                _lock.Release();
            }
        }

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

            Task.WaitAll(t1, t2);

            Console.WriteLine(_num);
        }
    }
}

SpinLock의 구현
하지만 정상작동하지 않음
 

public void Acquire()
        {
            // 잠김상태이면 무한루프로 풀리기를 기다린다.
            while(_locked)
            {
                
            }
            // 잠김이 풀렸으니 내가 사용
            _locked = true;
        }

잠김상태를 확인하고 잠겨있지 않았다면 내가 잠그는 행위가 원자적으로 이루어지지 않았기 때문에 발생하는 문제
 

class SpinLock
    {
        // _locked == true -> 잠김상태
        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.Exchange를 사용하여 문제 해결 (_locked 변수는 bool 타입에서 int 타입으로 변경하여 사용)
original = Interlocked.Exchange(ref _locked, 1)은 _locked 변수에 1을 대입하고 기존 _locked 변수를 original에 return 해준다.
즉, 기존 _locked값이 0이었다면 잠김상태가 아니란 뜻이니 1을 대입하여 잠가주고 break를 통해 빠져나오는 것이다.
 

{
    int original = _locked;
    _locked = 1;
    if (original == 0) break;
}

Acquire()의 작동방식 (Interlocked.Exchange)
 

public void Acquire()
        {
            while (true)
            {
                int original = Interlocked.CompareExchange(ref _locked, 1, 0);
                if (original == 0) break;
            }
        }

Interlocked.CompareExchange를 사용하여 문제를 해결할 수도 있다.
_locked과 0이 같아면 _locked에 1을 대입해주는 방식
 

{
    if (_locked == 0) _locked = 1;
}

Interlocked.CompareExchange의 작동방식 (CAS (Compare - And - Swap) 계열)
 

Thread.Sleep(1); // 무조건 휴식 => 1ms
Thread.Sleep(0); // 조건부 양보 => 나보다 우선순위가 낮은 애들한테는 양보 불가 => 우선순위가 나보다 같거나 높은 쓰레드가 없으면 다시 본인한테
Thread.Yield(); // 관대한 양보 => 관대하게 양보, 지금 실행이 가능한 쓰레드가 있으면 실행 => 실행 가능한 쓰레드가 없으면 남은시간 소진

일정 시간 뒤 다시시도를 구현하기 위해 셋중 하나를 선택하여 사용
 

class Program
    {
        static SpinLock _lock = new SpinLock();
        static void Main(string[] args)
        {
            bool lockTaken = false;

            try
            {
                _lock.Enter(ref lockTaken);
            }
            finally
            {
                if (lockTaken) _lock.Exit();
            }
        }
    }

SpinLock은 이미 구현이 되어있어 그대로 사용해도 된다.
내부적으로는 계속 시도를 하지만 계속 Lock 상태라면 Yield를 가끔 주어 양보를 하기도 한다.

'네트워크' 카테고리의 다른 글

ReaderWriterLock  (0) 2023.08.04
AutoResetEvent  (0) 2023.08.03
DeadLock  (0) 2023.08.01
Lock 기초  (0) 2023.07.31
Interlocked  (0) 2023.07.30

전의 DeadLock은 1개만 Lock을 하면 되는 상황에서 발생하는 경우였다.
만약 2개를 Lock 해야 하는 상황에서 1번 쓰레드가 1번을, 2번 쓰레드가 2번을 Lock을 하고 다음을 Lock 하려고 보니 이미 Lock이 되어 있는 상황이다. 결국 이도저도 못하고 DeadLock이 발생하게 된다.
 

class DeadLock1
{
    static object obj = new object();
    public static void Test1()
    {
        lock(obj)
        {
            DeadLock2.Test2();
        }
    }
}

class DeadLock2
{
    static object obj = new object();
    public static void Test2()
    {
        lock(obj)
        {
            DeadLock1.Test1();
        }
    }
}

DeadLock1.Test1() 메소드가 호출되어 obj에 대한 락을 획득한다.

DeadLock1.Test1() 메소드 내부에서 DeadLock2.Test2 메소드를 호출한다.

DeadLock2.Test2() 메소드는 또 다른 obj에 대한 락을 획득하려고 시도한다.

그러나 이미 DeadLock1.Test1()이 실행중이기 때문에 락을 획득할 수 없다.

DeadLock2.Test2()는 DeadLock1.Test1()이 obj 락을 해제할 때까지 기다린다.

문제는 DeadLock1.Test1()이 DeadLock2.Test2()가 끝날 때까지 대기 중이다.

두 메소드는 서로가 끝가리는 기다리면서 영원히 봉쇄된다.

 
해결방법 1.
Monitor.TryEnter()를 사용하여 일정시간 Lock을 획득하지 못하면 포기하게 만든다.
 
해결방법 2.
두 클래스의 함수가 정확히 같이 호출되어야 발생하므로 함수 호출 사이에 Thread.Sleep()을 준다.

'네트워크' 카테고리의 다른 글

AutoResetEvent  (0) 2023.08.03
SpinLock  (0) 2023.08.02
Lock 기초  (0) 2023.07.31
Interlocked  (0) 2023.07.30
메모리 배리어  (0) 2023.07.29
static void Thread1()
{
    for (int i = 0; i < 1000000; i++)
    {
        Monitor.Enter(obj);
        num++;
        Monitor.Exit(obj);
    }
}

static object obj = new object()로 오브젝트를 만들어주고 num++ 위 아래로 Moniter.Entor(obj), Monitor.Exit(obj)로 감싸주면 안의 코드는 상호 배제(Mutual Exclusive)로 돌아간다.

Monitor.Enter(obj)는 지정된 객체에 대한 잠금을 획득한다.

이 메서드를 호출한 쓰레드가 이미 잠금을 보유하고 있는 경우 그 쓰레드는 계속 진행 가능하다.

그렇지 않은 경우 이 메서드는 다른 쓰레드가 잠금을 해제할 때까지 현재 쓰레드를 차단한다.

 

num++은 임계 영역 내부에서 수행되므로 한 번에 하나의 쓰레드만 해당 연산을 수행 할 수 있다.

 

Monitor.Exit(obj)는 지정된 객체에 대한 잠금을 해제한다.

이 메서드를 호출하면 차단된 다른 쓰레드 중 하나가 잠금을 획득하고 실행을 계속할 수 있다.

 

 

static void Thread1()
{
    for (int i = 0; i < 1000000; i++)
    {
        Monitor.Enter(obj);
        num++;
        return;
        Monitor.Exit(obj);
    }
}

만약 Monitor.Enter(obj) 이후 return을 하게 되면 Exit가 없어 다른 쓰레드가 작업하지 못해 프로그램이 제대로 작동하지 않는다. (DeadLock)

 

 

static void Thread1()
{
    for (int i = 0; i < 1000000; i++)
    {
        try
        {
            Monitor.Enter(obj);
            num++;
            return;
        }
        finally
        {
            Monitor.Exit(obj);
        }
    }
}

해결방법 1.

Monitor.Enter(obj); num++; return;을 try로 감싸고 finally에서 Monitor.Exit(obj)를 넣어준다.

이 경우 return에 의해 함수가 종료되더라도 finally는 항상 실행되므로 Monitor.Exit(obj)가 실행되고 잠금이 해제된다.

이렇게 하면 예외가 발생하더라도 잠금이 해제되므로 다른 쓰레드가 잠금을 획득할 수 있다.

 

 

static void Thread1()
{
    for (int i = 0; i < 1000000; i++)
    {
        lock(obj)
        {
            num++;
            return;
        }
    } 
}

해결방법 2.

lock을 사용한다.

lock은 내부적으로 1과 같이 try finally로 돌아간다.

lock 블록 내의 코드가 예외를 발생시켜도 Monitor.Exit(obj)가 항상 호출되어 잠금이 제대로 해제된다.

Monitor.Enter(obj)와 Monitor.Exit(obj)를 사용할 때는 수동으로 관리를 해야 하는 부분이었다.

lock을 사용하면 코드가 더 간결해지고 가독성이 향상 되므로 주로 이를 사용한다.

'네트워크' 카테고리의 다른 글

SpinLock  (0) 2023.08.02
DeadLock  (0) 2023.08.01
Interlocked  (0) 2023.07.30
메모리 배리어  (0) 2023.07.29
컴파일러 최적화  (0) 2023.07.28
class Program
{
    static int num = 0;
    static void Thread1()
    {
        for (int i = 0; i < 10000; i++) num++;
    }
    static void Thread2()
    {
        for (int i = 0; i < 10000; i++) num--;
    }
    static void Main(string[] args)
    {
        Task t1 = new Task(Thread1);
        Task t2 = new Task(Thread2);
        t1.Start();
        t2.Start();

        Task.WaitAll(t1, t2);

        Console.WriteLine(num);
    }
}

이 프로그램은 2개의 쓰레드를 생성하고 num이라는 공유 변수에 대해 동시에 작업을 수행한다.

Thread1은 num을 10000 증가시키고, Thread2는 num을 10000 감소시킨다.

이론적으로는 두 연산이 끝난 후 num의 값은 0이어야 한다.

실제로 실행 시 0이 제대로 나옴을 확인 할  수 있었는데 여기서 횟수를 10만으로 바꾸어 실행을 해보면 -10만 ~ 10만의 랜덤값이 나옴을 확인 할 수 있었다.

 

이는 num++ / num-- 이 어셈블리어에서 실행되는 방식의 문제이다.

num++ / num-- 는 num 값을 읽어오는 부분, 증감하는 부분, 증감된 값을 num에 쓰는 부분으로 나눌 수 있다.

 

num = 0의 상황에서 Thread1과 Thread2를 동시에 1번 실행한다 하였을 때,

Thread1

temp = num // 0

temp +=1 // 1

num = temp // 1

 

Thread2

temp = num // 0

temp -= 1 // -1

num = temp // -1

의 상태가 되며 어느 쓰레드가 먼저 끝나냐에 따라 -1 or 1의 값을 가지게 된다.

 

혹은 Thread1이 num을 증가시키고 num에 다시 쓰기 전에 Thread2가 실행 된다면 Thread1의 증가 연산은 사실상 무시가 된다.

 

Interlocked.Increment(ref num);

이 때 Interlocked.Increment(ref num);를 사용하여 해결할 수 있다.

Interlocked.Increment(ref num);는 num이라는 정수값을 원자적으로 증가시키는 연산이다.

Interlocked.Increment(ref num);이 실행되면 num의 값이 1증가하며 이는 원자적으로 실행 된다.

즉, 이 연산은 중간에 다른 쓰레드에 의해 방해받지 않는다.

 

이렇게 Interlocked.Increment(ref num);와 Interlocked.Decrement(ref num);를 사용하면 여러 쓰레드에서 동일한 변수를 안전하게 증가시키거나 감소시킬 수 있다.

단, 성능에서 손해가 있다.

Interlacked를 사용했다면 volatile를 사용하지 않아도 된다.

 

'네트워크' 카테고리의 다른 글

DeadLock  (0) 2023.08.01
Lock 기초  (0) 2023.07.31
메모리 배리어  (0) 2023.07.29
컴파일러 최적화  (0) 2023.07.28
쓰레드 생성하기  (1) 2023.07.27
class Program
{
    static int x = 0, y = 0, r1 = 0, r2 = 0;
    static void Thread1()
    {
        y = 1;
        r1 = x;
    }
    static void Thread2()
    {
        x = 1;
        r2 = y;
    }
    static void Main(string[] args)
    {
        int count = 0;
        while(true)
        {
            count++;
            x = y = r1 = r2 = 0;

            Task t1 = new Task(Thread1);
            Task t2 = new Task(Thread2);
            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            if (r1 == 0 && r2 == 0) break;
        }
        Console.WriteLine($"{count}");
    }
}

Main에서 Thread1과 Thread2는 동시에 실행된다.

Thread1은 y를 1로 설정 후 x의 값을 r1에 복사하고, Thread2는 x를 1로 설정 후 y의 값을 r2에 복사한다.

이 때 싱글 쓰레드 환경이라면 r1과 r2 모두 0이 될 수 없을 것이다.

Thread1이 먼저 실행된다면 y = 1이 되고 Thread2에서 r2 = 1이 된다.

Thread2가 먼저 실행된다면 x = 1이 되고 Thread1에서 r1 = 1이 된다.

하지만 멀티 쓰레드 환경이라면 r1과 r2 모두 0이 될 수도 있다.

최적화에 의해 명령이 재정렬 될 수 있는데 예를 들어, Thread1에서 r1 = x가 y = 1보다 먼저 실행 될 수 있다.

그리고 Thread2에서는 r2 = y가 x = 1보다 먼저 실행 될 수 있다.

이 경우 r1과 r2가 모두 0이 될 수 있다.

Main에서는 무한 루프를 돌며 r1과 r2가 모두 0이 되는 사례가 발생할 때 까지의 시도 횟수를 출력한다.

 

이러한 현상을 방지하기 위해 메모리 배리어를 사용할 수 있다.

멀티 쓰레드 환경에서 서로 다른 쓰레드에서 실행되는 명령어들이 특정한 순서대로 실행되도록 강제하는 역할을 한다.

메모리 베리어를 설정하면 그 이전의 명령이 메모리 베리어 이후의 명령보다 먼저 완료되도록 강제한다.

 

 

static void Thread1()
{
    y = 1;
    Thread.MemoryBarrier();
    r1 = x;
}

static void Thread2()
{
    x = 1;
    Thread.MemoryBarrier();
    r2 = y;
}

위와 같이 y = 1과 r1 = x 중간에 Thread.MemoryBarrier()를 선언한다.

y = 1코드는 메모리 배리어에 막혀 아래로 내려갈 수 없고 r1 = x는 올라갈 수 없으니 실행 순서가 지켜지게 된다.

Thread2에서도 똑같이 메모리 배리어를 선언하면 처음 생각한대로 r1 = 0 && r2 == 0인 상황이 나올 수 없게 되고 Main은 무한 루프를 돌게 된다.

 

y = 1처럼 변수에 값을 넣는 Stroe와 r1 = x 처럼 변수를 불러오는 Load를 둘 다 막는 것을 Full Memory Barrier라 한다. (ASM MFENCE)

한쪽만 막고 싶은 경우 Store Memory Barrier (ASM SFENCE), Load Memory Barrier (ASM LFENCE)를 사용한다.

 

또한, y = 1 이후에 메모리 배리어를 만나면 y = 1을 캐시에 들고 있지 않고 바로 메모리에 업데이트를 해준다.

r1 = x의 경우, 업데이트 된 x의 값을 불러오게 된다.

'네트워크' 카테고리의 다른 글

DeadLock  (0) 2023.08.01
Lock 기초  (0) 2023.07.31
Interlocked  (0) 2023.07.30
컴파일러 최적화  (0) 2023.07.28
쓰레드 생성하기  (1) 2023.07.27
class Program
{
    static bool stop = false;

    static void ThreadMain()
    {
        Console.WriteLine("Thread Start");
        while (stop == false) { }
        Console.WriteLine("Thread End");
    }
    static void Main(string[] args)
    {
        Task t = new Task(ThreadMain);
        t.Start();

        Thread.Sleep(1000);

        stop = true;

        Console.WriteLine("stop true");
        Console.WriteLine("End Waiting");
        t.Wait();
        Console.WriteLine("End Success");
    }
}

bool 형태로 정적 변수 stop을 선언한 후 false로 설정한다.

ThreadMain 메서드에서는 Thread Start를 출력하고 stop이 ture가 될 때까지 기다린다.

true가 되었다면 Thread End를 출력하고 끝난다.

Main 메서드에서는 새로운 태스크 t를 생성하고 ThreadMain 메서드를 실행하도록 한다.

t.Start를 통해 태스크를 시작한다. 이 때 별도의 쓰레드에서 ThreadMain 메서드가 실행된다.

Thread.Sleep(1000)을 통해 메인 쓰레드를 1초 동안 일시 중단한다.

stop = true를 통해 ThreadMain의 무한 루프를 종료한다.

stop true와 End Waiting이 출력되고 t.Wait을 통해 ThreadMain 메서드가 종료될 때까지 기다린다.

Thread End가 출력되며 ThreadMain 메서드가 종료되고 이어서 End Success가 출력되며 메인 메서드가 종료된다.

 

중간에 Thread.Sleep(1000) 코드는 stop = true가 실행되기 전에 ThreadMain 메서드가 먼저 실행되도록 한 것이다.

해당 코드가 없으면 ThreadMain 메서드가 시작하기 전에 stop은 true로 바뀌어 무한 루프가 시작되지 않을 것이다.

 

 

위의 코드를 디버그 모드에서 실행하면 생각한대로 잘 되지만 릴리즈 모드에서 실행하면 End Waiting에서 종료되지 않는다.

 

 

이는 컴파일러가 while (stop == false) {} 을

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

로 최적화를 해주었기 때문이다.

While안에서 stop이 변하지 않기 때문에 위와 같이 최적화를 해도 문제가 없지만 이 경우 stop은 While안이 아닌 Main함수에서 값을 변경하고 있어 문제가 되었다.

 

 

volatile static bool stop = false;

멀티 쓰레드 환경에서 동일한 변수를 공유할 때 캐시에 저장된 값과 메모리에 저장된 값이 달라 오류가 생길 수도 있고 위와 같이 최적화 문제가 생길 수도 있다.

이런 문제를 해결하기 위해서 volatile 키워드를 사용한다.

volatile을 붙이면 컴파일러에게 해당 변수에 대한 최적화를 제한하라고 지시하고, 변수의 값을 캐시에 저장하지 않고 메인 메모리에서 값을 읽어오도록 한다.

 

 

volatile을 붙인 후 실행해 보면 릴리즈 모드에서도 프로그램이 정상 종료 됨을 볼 수 있다.

'네트워크' 카테고리의 다른 글

DeadLock  (0) 2023.08.01
Lock 기초  (0) 2023.07.31
Interlocked  (0) 2023.07.30
메모리 배리어  (0) 2023.07.29
쓰레드 생성하기  (1) 2023.07.27
using System;
using System.Threading;

쓰레드를 사용하기 위해 using System.Threading를 선언해준다.

 

 

 class Program
{
    static void MainThread()
    {
        Console.WriteLine("Hello Thread!");
    }
    static void Main(string[] args)
    {
        Thread t = new Thread(MainThread);
        t.Start();
        Console.WriteLine("Hello World!");
    }
}

Hello Thread!를 출력하는 MainThread 메소드를 정의한다.

Main 메소드 내에서 Thread 클래스의 인스턴스 t를 생성한다.

이 때 MainThread를 인자로 넘겨주면 t 쓰레드가 시작될 때 MainThread 메소드가 실행된다.

t.Start()로 t 쓰레드를 시작한다.

이러면 MainThread 메소드를 별도의 쓰레드에서 실행하게 한다.

메인 쓰레드와 t 쓰레드가 동시에 실행되며 각각 Hello World!와 Hello Thread!를 출력한다.

출력 순서는 운영 체제의 스케쥴링에 따라 달라질 수 있다.

 

 

 

class Program
{
    static void MainThread()
    {
        while(true) Console.WriteLine("Hello Thread!");
    }
    static void Main(string[] args)
    {
        Thread t = new Thread(MainThread);
        t.IsBackground = true;
        t.Start();
        Console.WriteLine("Hello World!");
    }
}

쓰레드는 디폴트로 포그라운드에서 실행되기 때문에 MainThread에 무한루프를 걸어주면 위의 프로그램은 종료되지 않고 계속 돌아간다.

쓰레드를 백그라운드에서 실행하려면 IsBackground를 true로 설정하면 된다.

백그라운드 쓰레드는 메인 쓰레드가 종료되면 자동적으로 종료되는 쓰레드를 의미한다.

 

 

메인 쓰레드의 종료와 함께 Hello Thread!가 끝난 것을 볼 수 있다.

백그라운드로 실행하는 것을 기다리려면 Join()을 사용하면 된다.

 

ThreadPool을 사용하면 Thread를 생성하지 않고 사용할 수 있다.

ThreadPool.QueueUserWorkItem(MainThread);

ThreadPool.QueueUserWorkItem 메소드는 .NET의 ThreadPool 클래스에서 제공하는 메소드로, 새 쓰레드를 직접 생성하고 관리하는 대신 쓰레드 풀의 쓰레드를 사용하여 작업을 비동기적으로 실행하도록 예약하는데 사용된다.

ThreadPool의 QueueUserWorkItem을 사용하기 위해서는 MainThread에 매개변수가 필요하다.

ThreadPool은 디폴트로 백그라운드에서 실행된다.

SetMinThreads와 SetMaxThreads를 사용하여 여러개의 쓰레드를 미리 생성하여 사용할 수 있다.

 

 

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

using System.Threading.Tasks;를 선언하여 Task 사용 가능하다.

Task는 내부적으로 ThreadPool을 사용한다.

 

class Program
{
    static void MainThread(object state)
    {
        Console.WriteLine("Hello Thread!");
    }
    static void Main(string[] args)
    {
        ThreadPool.SetMinThreads(1, 1);
        ThreadPool.SetMaxThreads(1, 1);
        Task t = new Task(() => { while (true) { } });
        t.Start();
        ThreadPool.QueueUserWorkItem(MainThread);
        while (true) { }
    }
}

ThreadPool의 Thread를 1개로 설정하고 무한루프 Task t를 실행하면, ThreadPool.QueueUserWorkItem(MainThread)에 할당할 쓰레드가 없어 실행이 되지 않는다.

 

 

Task t = new Task(() => { while (true) { } }, TaskCreationOptions.LongRunning);

오래 걸리는 일이라면 TaskCreationOptions.LongRunning을 사용한다.

이를 사용하면 ThreadPool에서 쓰레드를 가져오지 않고 새로 생성하기 때문에 ThreadPool.QueueUserWorkItem(MainThread)가 실행 된다.

'네트워크' 카테고리의 다른 글

DeadLock  (0) 2023.08.01
Lock 기초  (0) 2023.07.31
Interlocked  (0) 2023.07.30
메모리 배리어  (0) 2023.07.29
컴파일러 최적화  (0) 2023.07.28

+ Recent posts