[c#/네트워크] TCP와 UDP 차이
TCP와 UDP의 차이에 대해 알아보자 📌 TCP와 UDP 차이 프로토콜 종류 연결 지향성 신뢰성 속도 특징 TCP 연결형 서비스 좋음 느림 1) 연결을 위해 할당되는 논리적인 경로가 있다. 2) 전송 순서가 보장된다. 3) 분실이 일어나면 책임지고 다시 전송한다(신뢰성 👍) 4) 물건을 주고 받을 상황이 아니면 일부만 보낸다(흐름/혼잡 제어) 5) 고려할 것이 많으니 속도가 느리다 UDP 비연결형 서비스 나쁨 빠름 1) 연결이라는 개념이 없다. 2) 전송 순서가 보장되지 않는다. 3) 분실에 대한 책임이 없다(신뢰성 👎) 4) 일단 보내고 생각한다 5) 단순하기 때문에 속도가 빠르다 데이터 전송 보장 및 순서 보장을 위해서는 tcp를 주로 사용한다. tcp는 3way handshake 방식이다. 🔎 3..
2022.12.22
[c#/디자인패턴] - 싱글톤(Singleton) 패턴
📌싱글톤 패턴 : 클래스의 인스턴스를 단 하나만 생성하고자 할 때 사용한다 게임 개발에서 주로 게임을 관리하는 매니저 계열의 클래스를 만들 때 적합하며, (딱 하나만 필요) 사운드를 재생하려 할 때, 관련된 정보를 어디에서나 알게 하여 누구나 재생할 수 있도록 하기 위해(전역적 접근) 사용한다고 생각하면 된다. ⭐ 특징 Lazy 초기화(Lazy initialization) : 선언 즉시 인스턴스를 생성하는 것이 아니라, 처음 접근할 때 객체를 생성한다. 상속 가능 : 하위 클래스 오브젝트도 모두 싱글톤이 된다. 1. 안전하지 않은 버전 - not thread-safe public class SingletonClass { private SingletonClass() { } //클래스의 외부에서 함부로 객체..
2022.12.20
no image
[c#/네트워크] listener 클래스를 활용한 비동기 소켓 프로그래밍
앞서 진행한 소켓 프로그래밍 구현에서 ServerCore 폴더에 Listener 파일을 추가한다. 문지기 즉, 소켓을 만드는 것을 따로 파일로 빼서 관리할 예정이다. 생성된 Listener 클래스 안에 소켓을 생성한다. Socket _listenSocket; 소켓을 초기화 하는 함수는 따로 만들 것이다. 기존 코드를 보면 (빨간 줄 무시) endPoint를 받아서 소켓을 생성하고 Bind도 하는 것을 볼 수 있다. (*기존 코드 : 전 글 확인..) 따라서 Init함수 인자로 endPoint를 넣어주며 Init() 함수를 다음과 같이 만들어 준다. Init() public void Init(IPEndPoint endPoint) { _listenSocket = new Socket(endPoint.Addre..
2022.12.13
no image
[c#/네트워크] 소켓 프로그래밍
소켓 프로그래밍을 구현해 보겠다. 다음과 같이 파일 구조를 만든다.. 그 다음 솔루션 우클릭 후, 속성에 들어가서 클라이언트와 서버가 한 번에 실행되도록 다음과 같이 설정한다. 이제 서버부터 구현해 보자. 일단 연결을 시도하려는 자에 대한 정보를 얻어야 한다. //DNS (Domain Name System) //도메인 등록 후 -> IP주소 찾게끔 만들면 유지보수가 용이함 //PC의 호스트명을 받는다 (호스트명은 IP 주소 대신 사용할 수 있는 식별 이름) string host = Dns.GetHostName(); IPHostEntry ipHost = Dns.GetHostEntry(host); //해당 호스트의 IP 관련 주소들을 받는다. IPAddress ipAddr = ipHost.AddressLis..
2022.12.12
[c#/유니티] 네트워크 기초 - 스위치와 라우터
1️⃣ 라우터 : 서로 다른 네트워크를 연결하는 역할을 한다. (네트워크 간) IP주소를 사용하여 데이터 전송을 수행한다. 2️⃣ 스위치 : 같은 네트워크 안에서 데이터 전송을 한다. (네트워크 내) LAN 포트 사이에서 데이터 전송을 하며 MAC 주소를 사용한다. 🤔 MAC와 IP 차이? 📌 MAC 주소 (Media Access Control Address) 하드웨어 고유 주소, 48비트 16진법을 사용한다. 하드웨어 제조 업체가 지정. DC-21-5C-3C-6B-C6 다음과 같은 주소에 해당한다. 📌 ip 주소(Internet Protocol) : 네트워크 관리자 혹은 인터넷 서비스 공급자(ISP)에 의해 제공되는 주소 네트워크 연결을 위해 제공되는 주소 IPv4 주소는 32 비트 주소이고 IPv6 ..
2022.12.12
[c#/유니티] ReaderWriterLock 구현 하기
📌  ReaderWriterLock 이란?            읽기 전용 작업(Read-Only) 에 대해서는 동시 접근을 허용하고, 쓰기 작업에 대해서는 독점 접근(exclusive)을 함.              🤔 왜, 언제 사용할까 ❗❓                  lock을 사용하면 오직 하나의 스레드만 코드(임계영역)에 접근할 수 있다.                  하지만 하나의 스레드만이 쓰고 읽을 수 있다면 매우 시간도 오래걸리고 비효율적일 것이다.          👉 ReaderWriterLock은 이를 해결해준다. 다수의 스레드들이 읽을 수 있게 하며, 쓰기 전용 권한을 가질 수 있도록 한다.                 다수의 독자가 있고 작성자가 적은 경우에 효율적이다.  ..
2022.12.09
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
320x100
728x90

 

TCP와 UDP의 차이에 대해 알아보자

 

 

 

📌 TCP와 UDP 차이 

프로토콜 종류 연결 지향성 신뢰성 속도 특징
TCP 연결형 서비스  좋음 느림 1) 연결을 위해 할당되는
     논리적인 경로가 있다.

2) 전송 순서가 보장된다.

3) 분실이 일어나면 책임지고
    다시 전송한다(신뢰성 👍)

4) 물건을 주고 받을 상황이 아니면
    일부만 보낸다(흐름/혼잡 제어)

5) 고려할 것이 많으니 속도가 느리다
UDP 비연결형 서비스 나쁨  빠름 1) 연결이라는 개념이 없다.

2) 전송 순서가 보장되지 않는다

3) 분실에 대한 책임이 없다(신뢰성 👎)

4) 일단 보내고 생각한다

5) 단순하기 때문에 속도가 빠르다

 

 

 

데이터 전송 보장 및 순서 보장을 위해서는 tcp를 주로 사용한다.

tcp3way handshake 방식이다.

 

 

 

🔎 3-way-handshake 

송신 A쪽에서 connect request를 보내면, 
수신 B 쪽에서 Ok 신호를 보냄 


이 때, 보낸 Ok 신호가 잘 갔는 지 의문이 생길 수 있다 
그 때 송신한 A가 잘 받았다는 신호를 전달한다. 


이렇게 세 번 전송하는 것이 3-way handshake이다

 

 

 

🔎 연결이 끊기는 경우 2가지

 

  • 응답이 안오는 경우

B에서 Ok 신호를 보내고 응답이 없어서 다시 한번 Ok 신호를 보내도 답이 없다면

이 상황을 연결이 끊어졌다 라고 보는 것이다. 

 

  • Disconncect를 사용하는 경우

A 쪽에서 전송을 끊는 경우 

 

 

👉 연결이 되고 끊어지고를 프로그램이 정확히 알 수 있다. 

 

 

 

 

🔎 송,수신 보장 

 

송신 쪽에서 4개의 패킷을 보낸다 하자.

수신 쪽에서 어떤 순서로 왔는 지 알 수 없기 때문에 

1,2,3,4 라는 순서를 적어 보낸다. 

 

만약 수신 B가 1,2,4만 받았다면 

3번을 받지 못했다고 송신 A에게 알려준다. 

 

👉단점 : 지연 시간 김

 

 

 

 

udp 

위 3-way handshake는 3번을 거치기 때문에 3배 느리다는 단점이 있다. 

하지만 게임에서는 반응이 빨라야 하므로 UDP를 쓰고 싶어 한다. (지연 시간 짧음)

but.. 순서 보장이 안됨. 데이터 유실 가능성 있음 

 

 

 

 

 

⭐이를 해결하기 위해 게임 개발자들이 만든 것이 reliable udp 

tcp 비효율적인 속도 면을 향상시키고, UDP의 불안정성을 해결한다. 

 

 

728x90
반응형
320x100
728x90

 

📌싱글톤 패턴 : 클래스의 인스턴스를 단 하나만 생성하고자 할 때 사용한다 

 

 

게임 개발에서 주로 게임을 관리하는 매니저 계열의 클래스를 만들 때 적합하며, (딱 하나만 필요)

사운드를 재생하려 할 때, 관련된 정보를 어디에서나 알게 하여 누구나 재생할 수 있도록 하기 위해(전역적 접근) 

사용한다고 생각하면 된다. 

 

 

특징

  • Lazy 초기화(Lazy initialization) : 선언 즉시 인스턴스를 생성하는 것이 아니라, 처음 접근할 때 객체를 생성한다. 
  • 상속 가능 : 하위 클래스 오브젝트도 모두 싱글톤이 된다. 

 

 

 

1. 안전하지 않은 버전 - not thread-safe 

public class SingletonClass
{
    private SingletonClass() { }    //클래스의 외부에서 함부로 객체를 만들 수 없게 함 

    private static SingletonClass instance;    //객체 생성 이후 생성된 것만 꺼내서 사용함 
    public static SingletonClass Instance
    {
        get
        {
            if(instance == null) //인스턴스가 비어있는 경우에만 -> 생성되지 않았을 때, 
            { 
                instance = new SingletonClass();   //새 객체를 생성해서 instance에 변수에 넣어줌
            }
            return instance;
        }
    }
}

 

단 하나의 인스턴스만 생성해야 하므로 외부에서 객체를 생성할 수 없도록 private 접근 지정자를 설정한다. 

인스턴스가 null인지를 체크하여 비어있을 때만 새 객체를 만든다.

 

 

하지만 이 코드는 스레드로부터 안전하지 않다. 

서로다른 A,B 스레드가 모두 if(instance == null) 조건을 만족한다면, A와 B 모두 인스턴스를 생성하게 되어

싱글톤 패턴을 위반하기 때문이다. 

 

 

 

그렇다면 어떻게 해결할까🤔..?

 

  • lock을 사용하는 경우
  • lock을 사용하지 않는 경우
  • lazy를 활용하는 경우

 

 

 

 

2. 락(lock)을 사용한 싱글톤 - simple thread-safety

 

public sealed class Singleton
{
    private static Singleton instance = null;
    private static readonly object padlock = new object();

    Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            lock (padlock)
            {
                if (instance == null)
                {
                    instance = new Singleton();
                }
                return instance;
            }
        }
    }
}

 

lock을 사용하면 인스턴스를 생성하기 전에 인스턴스가 미리 생성되었는 지 여부를 확인한다.

A 스레드가 먼저 진입하여 인스턴스를 생성하였다면, B 스레드가 그 다음으로 진입할 때 if(instance==null)가 false이기 때문에 인스턴스가 또 생성되지 않는다. 

 

 

 

 

3. lock을 사용하지 않지만 - thread-safe 한, 약간 lazy 한 

public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();

    static Singleton()
    {
    }

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            return instance;
        }
    }
}

 

static 생성자는 클래스의 인스턴스가 생성되거나 정적 멤버가 참조될 때만 실행되고 앱 도메인당 한 번만 실행되도록 지정된다. lock을 사용하는 것 보다는 성능면에서 더 빠르다. 

 

 

 

 

4. 완전한 lazy 인스턴스화

public sealed class Singleton
{
    private Singleton()
    {
    }

    public static Singleton Instance { get { return Nested.instance; } }

    private class Nested
    {
        static Nested()
        {
        }
        internal static readonly Singleton instance = new Singleton();
    }
}

 

 

 

 

5. using .NET 4's Lazy<T> type - 제네릭 사용

public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy =
        new Lazy<Singleton>(() => new Singleton());

    public static Singleton Instance { get { return lazy.Value; } }

    private Singleton()
    {
    }
}

 

 

 

 

3번의 ( lock을 사용하지 않지만 - thread-safe 한, 약간 lazy 한 )의 방법을 가장 추천한다고 한다. 

혹은 5번..

 

싱글톤 사용이 마냥 좋은 것은 아니다. 단점 또한 존재한다. 

 

 

 

💥 단점 

  • 정적 변수를 사용한다 ➡ 모든 클래스에서 접근할 수 있기 때문에, 전혀 상관 없는 클래스와 결합(coupling) 생김 
  • 게임 플레이에 있어서 게으른 초기화는 화면 프레임이 떨어지거나 화면 끊김 현상을 발생시킬 수 있다. 

 

 

 

 

자세한 정리와 원글은

참고 : https://dev-nicitis.tistory.com/4

https://csharpindepth.com/articles/singleton

728x90
반응형

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

[c#/자료구조] 배열(Array)  (0) 2022.12.06
320x100
728x90

 

앞서 진행한 소켓 프로그래밍 구현에서 ServerCore 폴더에 

Listener 파일을 추가한다. 

 

 

문지기 즉, 소켓을 만드는 것을 따로 파일로 빼서 관리할 예정이다. 

 

 

 

 

 

생성된 Listener 클래스 안에 소켓을 생성한다. 

Socket _listenSocket;

 

 

 

 

소켓을 초기화 하는 함수는 따로 만들 것이다. 

기존 코드를 보면 (빨간 줄 무시)

 

 

 

 

endPoint를 받아서 소켓을 생성하고 Bind도 하는 것을 볼 수 있다. 

(*기존 코드 : 전 글 확인..)

 

 

 

따라서 Init함수 인자로 endPoint를 넣어주며 Init() 함수를 다음과 같이 만들어 준다. 

 

 

 

Init()

public void Init(IPEndPoint endPoint)
{
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, 
    ProtocolType.Tcp);

    //문지기 교육 
    _listenSocket.Bind(endPoint);

    //영업 시작
    //backlog : 최대 대기수 
    _listenSocket.Listen(10);

}

 

 

 

그 후 Accept는 따로 Accept 함수를 만들어 빼준다. 

 

 

public Socket Accept()
{
    return _listenSocket.Accept();
}

 

 

 

 

이제 Program.cs 파일로 돌아가서 문지기 역할을 하는 Listener를 생성해준다. 

그리고 문지기를 초기화 하는 코드는 listener의 Init 함수와 Accept 함수 호출로 대체해준다. 

class Program
{

    static Listener _listener = new Listener();

    static void Main(string[] args)
    {
        ... 

        _listener.Init(endPoint,OnAcceptHandler);
        Console.WriteLine("Listening...");

        while (true)
        {
            Socket clientSocket = _listener.Accept();
        }
        
    }
    
}

 

하지만 현재 형태는 블로킹 함수로 이루어진 상황이다. 

이런 경우 사용자가 10000명이라고 가정했을 때, 무한 대기를 계속 하고 있는 상황이 발생할 수 있으므로

논 블로킹 형태로 구현해야 한다. 

 

 

 

(*블로킹 : 요청받는 함수가 작업을 모두 마치고 나서야 요청자에게 제어권을 넘겨줌 (그동안 요청자는 아무것도 하지않고 기다림)

                                                                    논블로킹 : 요청받은 함수가 요청자에게 제어권을 바로 넘겨줌 (그동안 요청자는 다른 일을 할 수 있음)

 

 

 

 

 

따라서 Accept , Receive, Send는 모두 비동기 형식 (논 블로킹) 으로 바꿔보자 

 

 

 

 

 


 

 

 

 

AcceptAsync()로 비동기 형태로 만들어준다.

이렇게 되면, 클라이언트 요청이 올때까지 무작정 기다리는 것이 아니라 다른 작업을 쭉 수행하다가

요청이 들어오면 그 때 CallBack 방식으로 알려주는 형태이다. 

public Socket Accept()
{
    _listenSocket.AcceptAsync();
    return _listenSocket.Accept();
}

 

 

 

좀 더 세부적으로 함수를 만들어보자.

Accpet 메소드 안에 있는 _listenSocket.AcceptAsync() 코드를 지우고 RegisterAccept 매소드 안에 다음과 같이 넣는다.

요청 여부에 대한 리턴값을 pending에 할당하여 확인하면서 그 값이 false이면 상황을 종료하면 된다. 

 

void RegisterAccept(SocketAsyncEventArgs args)
{
    bool pending = _listenSocket.AcceptAsync(args);
    if (pending == false)
        OnAcceptCompleted(null, args);

}

void OnAcceptCompleted(SocketAsyncEventArgs args)
{
    ...
}

 

 

 

pending이 true일 때는, 아까 말한 거꾸로 메시지를 알려주도록 하는 작업을 해보자. 

초기화 함수에 넣어주어 처음 시작 때 무조건 한번 실행되도록 할 것이다. 

public void Init(IPEndPoint endPoint)
{
    ...      //(위 코드 참고)

    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
    args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
    RegisterAccept(args);

}

 

 

 

EventHandler를 사용하기 위해 콜백 함수인 OnAcceptCompleted를 넣어주고, 

형식을 맞춰주기 위해 object sender라는 인자를 추가한다. 

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.SocketError == SocketError.Success)
    {
        //TODO
        _onAcceptHandler.Invoke(args.AcceptSocket);
    }else
        Console.WriteLine(args.SocketError.ToString());

    RegisterAccept(args);
}

 

 

 

 

accept가 되면 그 이후에 데이터를 보내고 받는 작업을 TODO에서 진행한다. 

아래 작업을 수행한다고 보면 된다. 

 

 

 

 

 

accept가 완료 됐을 때 이를 처리하는 변수를 Action으로 만들어준다. 

다음과 같이 _onAcceptHandler를 만들자. 

 

 

 

 

 

그리고 Init 함수에서 onAcceptHandler를 인자로 추가하고 핸들러를 연결해준다. 

public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
{
    _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, 
    ProtocolType.Tcp);
    _onAcceptHandler += onAcceptHandler;


    //문지기 교육 
    _listenSocket.Bind(endPoint);
    

    //영업 시작
    //backlog : 최대 대기수 
    _listenSocket.Listen(10);
    
    
    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
    args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
    RegisterAccept(args);

}

 

 

 

그리고 OnAcceptCompleted 매소드를 다음과 같이 수정한다.  

_onAcceptHandler를 Invoke 시키고 소켓을 넣어주면 된다. 

 

Socket clientSocket = listenSocket.Accept();

 

원래 Accept할 때 받은 소켓을 전달해 주는 것이다. 

void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
    if (args.SocketError == SocketError.Success)
    {
        //TODO
        _onAcceptHandler.Invoke(args.AcceptSocket);
    }else
        Console.WriteLine(args.SocketError.ToString());

    RegisterAccept(args);
}

 

 

 

_onAcceptHandler의 Invoke 결과는 다시 프로그램 클래스에게 전달해 주어야 한다. 

다음과 같이 프로그램안에 넣어준다. 

 

static void OnAcceptHandler(Socket clientSocket)
{
    try
    {
        //받는다
        byte[] recvBuff = new byte[1024];
        int recvBytes = clientSocket.Receive(recvBuff);
        String recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
        Console.WriteLine($"[From Client] {recvData}");

        //보낸다
        byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
        clientSocket.Send(sendBuff);

        //쫓아낸다.
        clientSocket.Shutdown(SocketShutdown.Both);
        clientSocket.Close();
    }
    catch(Exception e)
    {
        Console.WriteLine(e.ToString());
    }
}

 

 

 

Main 문은 다음과 같이 수정한다. 

리스너를 통해 초기화 한 부분에 OnAcceptHandler를 넣어준다. 

 

클라이언트가 들어오면 onAcceptHandler를 통해 알려달라는 의미이다. 

static void Main(string[] args)
{
    ...

    _listener.Init(endPoint,OnAcceptHandler);

}

 

 

중요한 것..! 

이벤트를 재사용 하기 위해서는 소켓을 null로 초기화 해 주어야 한다. 

RegisterAccept 메소드에 다음과 같이 추가하자 

 

args.AcceptSocket = null; //이벤트 재사용 시, 초기화 필수

 

 


 

 

 

최종 코드 

 

Program.cs

더보기
class Program
    {

        static Listener _listener = new Listener();

        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                //받는다
                byte[] recvBuff = new byte[1024];
                int recvBytes = clientSocket.Receive(recvBuff);
                String recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                Console.WriteLine($"[From Client] {recvData}");

                //보낸다
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
                clientSocket.Send(sendBuff);

                //쫓아낸다.
                clientSocket.Shutdown(SocketShutdown.Both);
                clientSocket.Close();
            }
            catch(Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
        static void Main(string[] args)
        {
            //DNS (Domain Name System)
            //도메인 등록 후 -> IP주소 찾게끔 만들면 유지보수가 용이함 
            string host = Dns.GetHostName();    //PC의 호스트명을 받는다 (호스트명은 IP 주소 대신 사용할 수 있는 식별 이름) 

            IPHostEntry ipHost = Dns.GetHostEntry(host);  //해당 호스트의 IP 관련 주소들을 받는다. 
            IPAddress ipAddr = ipHost.AddressList[0];   //여러개의 주소를 담고있는 리스트를 반환하며 이중 첫번째 값을 할당한다(여러개의 주소를 뱉어주는 경우도 있음) 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);  //7777은 포트 번호 , IP 주소와 포트 번호를 할당한다. 



            _listener.Init(endPoint,OnAcceptHandler);
            Console.WriteLine("Listening...");

            while (true)
            {
                ;
                   
            }
            
            }

        }

 

 

Listener.cs

더보기
class Listener
    {
        Socket _listenSocket;
        Action<Socket> _onAcceptHandler;

        public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler)
        {
            _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            //문지기 교육 
            _listenSocket.Bind(endPoint);

            //영업 시작
            //backlog : 최대 대기수 
            _listenSocket.Listen(10);

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
            RegisterAccept(args);

        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            args.AcceptSocket = null; //이벤트 재사용 시, 초기화 필수 

            bool pending = _listenSocket.AcceptAsync(args);
            if (pending == false)
                OnAcceptCompleted(null, args);

        }
        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                //TODO
                _onAcceptHandler.Invoke(args.AcceptSocket);
            }else
                Console.WriteLine(args.SocketError.ToString());

            RegisterAccept(args);
        }


        public Socket Accept()
        {
            return _listenSocket.Accept();
        }
    }

 

 

클라이언트는 동일  

 

 

 

출처 : https://joooing.tistory.com/entry/동기비동기-블로킹논블로킹

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

728x90
반응형
320x100
728x90

 

 

 

소켓 프로그래밍을 구현해 보겠다. 

 

 

다음과 같이 파일 구조를 만든다..

 

 

그 다음 솔루션 우클릭 후, 속성에 들어가서 

클라이언트와 서버가 한 번에 실행되도록 다음과 같이 설정한다. 

 

 

 


 

 

 

 

 

이제 서버부터 구현해 보자.

일단 연결을 시도하려는 자에 대한 정보를 얻어야 한다.

 

 //DNS (Domain Name System)
 //도메인 등록 후 -> IP주소 찾게끔 만들면 유지보수가 용이함
 //PC의 호스트명을 받는다 (호스트명은 IP 주소 대신 사용할 수 있는 식별 이름) 
string host = Dns.GetHostName();    


IPHostEntry ipHost = Dns.GetHostEntry(host);  //해당 호스트의 IP 관련 주소들을 받는다. 
IPAddress ipAddr = ipHost.AddressList[0];   //여러개의 주소를 담고있는 리스트를 반환하며 이중 첫번째 값을 할당한다(여러개의 주소를 뱉어주는 경우도 있음) 
IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);  //7777은 포트 번호 , IP 주소와 포트 번호를 할당한다.

 

  • IPHostEntry : DNS 클래스의 GetHostEntry의 반환 값을 담는 컨테이너 클래스. 호스트 관련 정보와 IP관련 정보를 받는다.
  • IPAddress : 하나의 인터페이스에 대한 주소를 포함하는 클래스. 첫번 째 인덱스에 담긴 IP 주소를 할당한다.
  • IPEndPoint : IP 주소와 포트를 바인딩하여 소켓과 원격 주소를 연결한다. 

 

 

 

 

 

서버 코드 

 

    //문지기 
    Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

    try{

        //문지기 교육
        listenSocket.Bind(endPoint);


        // 영업 시작
        // backlog : 최대 대기수
        listenSocket.Listen(10);

         while (true)
         {
             Console.WriteLine("Listening....");

             // 손님을 입장시킨다
             Socket clientSocket = listenSocket.Accept();

             // 받는다 
             byte[] recvBuff = new byte[1024];
             int recvBytes = clientSocket.Receive(recvBuff);
             string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
             Console.WriteLine($"[From Client] {recvData}");


             // 보낸다
             byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server !");
             clientSocket.Send(sendBuff);

             // 쫒아낸다 
             clientSocket.Shutdown(SocketShutdown.Both);
             clientSocket.Close();

        }
    }catch(Exception e)
    {
        Console.Write(e.ToString());
    }
  1. 문지기를 만든다 -> 서버 소켓을 만든다 (IP관련 정보와 소켓 타입, 연결 방법(프로토콜 설정)을 결정한다. 
  2. endPoint를 바인드 한다 (* endPoint : IP주소와 포트 번호의 조합)
  3. 클라이언트를 입장 시키고, 클라이언트가 전달하려는 내용을 받는다. (문자열을 전달하는 간단 예제)
  4. 클라이언트에게 서버 응답을 전달한다 
  5. 후 연결을 종료한다                                                                                                                                                                   (* shut down : 현재 연결된 프로세스의 개수에 상관없이 연결을 종료한다. close: 연결된 개수가 0이 되어야지만 연결이 종료된다. 소켓을 메모리 상에서 완전 해제하려면 shutdown 후에 close를 해 줘야 한다)

 

 

 

 

 

⭐ 클라이언트 코드

 

    //클라이언트 소켓 설정 
    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

    try{

        // 문지기에게 입장 문의 
        socket.Connect(endPoint);
        Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}");

        // 보낸다 
        byte[] sendBuff = Encoding.UTF8.GetBytes("Connected Client !");
        int sendBytes = socket.Send(sendBuff);

         // 받는다
         byte[] recvBuff = new byte[1024];
         int recvBytes = socket.Receive(recvBuff);
         string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
        Console.WriteLine($"[From Server] {recvData}");


         // 종료
         socket.Shutdown(SocketShutdown.Both);
         socket.Close();

    }catch(Exception e)
    {	
        Console.Write(e.ToString);
    }

 

 

실행 화면

 

서버 실행 화면

 

 

클라이언트 실행 화면

 

 

 

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

728x90
반응형
320x100
728x90

 

 

1️⃣ 라우터  : 서로 다른 네트워크를 연결하는 역할을 한다. (네트워크 )

  • IP주소를 사용하여 데이터 전송을 수행한다. 

 

 

 

2️⃣  스위치 : 같은 네트워크 에서 데이터 전송을 한다.  (네트워크 )

  • LAN 포트 사이에서 데이터 전송을 하며 MAC 주소를 사용한다.

 

 

 

🤔 MACIP 차이?

 

 

📌  MAC 주소 (Media Access Control Address)

  • 하드웨어 고유 주소, 48비트 16진법을 사용한다. 
  • 하드웨어 제조 업체가 지정.
  • DC-21-5C-3C-6B-C6 다음과 같은 주소에 해당한다. 

 

 

 

📌  ip 주소(Internet Protocol) :   

  • 네트워크 관리자 혹은 인터넷 서비스 공급자(ISP)에 의해 제공되는 주소 
  • 네트워크 연결을 위해 제공되는 주소
  • IPv4 주소는 32 비트 주소이고 IPv6 은 128 비트 주소

 

 

 

 

👉  IP 주소는 네트워크에 있는 장치에 대한 연결을 식별, MAC 주소는 네트워크에 참여하는 장치 자체를 식별

 

 

 

728x90
반응형
320x100
728x90

 

📌  ReaderWriterLock 이란? 

           읽기 전용 작업(Read-Only) 에 대해서는 동시 접근을 허용하고, 쓰기 작업에 대해서는 독점 접근(exclusive)을 함.  

 

 

          🤔 왜, 언제 사용할까 ❗❓

 

                 lock을 사용하면 오직 하나의 스레드만 코드(임계영역)에 접근할 수 있다. 

                 하지만 하나의 스레드만이 쓰고 읽을 수 있다면 매우 시간도 오래걸리고 비효율적일 것이다.

 

         👉 ReaderWriterLock은 이를 해결해준다. 다수의 스레드들이 읽을 수 있게 하며, 쓰기 전용 권한을 가질 수 있도록 한다.                 다수의 독자가 있고 작성자가 적은 경우에 효율적이다. 

 

 

              Read :  다수의 스레드가 접근 가능

                          다른 쓰레드가 쓰고(Write)있으면 대기 -> Write Lock이 해제 되어야 진입 가능

 

              Write :  동시에 하나의 스레드만 수정(접근) 가능

                           읽고 있는 중에 데이터가 수정되면 안되므로, 모든 Read Lock이 해제되어 있는 상황이어야 함

                                   -> ReadLock이 해제될 때 까지 대기

 

 

 

 

 

💥  ReaderWriterLock 구현하기 

 

        👉  재귀적 Lock을 허용하지 않는 상황 

                 설정 :  스핀락 사용, 최대 스핀 횟수(MAX_SPIN_COUNT) 는 오천번으로 설정

                            32비트를 사용한다. 

 

 

 

                 찬찬히 코드를 살펴보자 

 

 


 

 

             1️⃣  변수 및 플래그 설정 

 

                    EMPTY_FLAG 는 플래그 기본 값,

                    WRITE_MASK는 1부터 15번째의 비트(15개),

                    READ_MASK 는 16부터 31번째 비트를 사용한다(16개)

 

                    MAX_SPIN_LOCK은 스핀락 최대 시도 횟수로 5천번으로 제한해 두었다. 

                    _flag 값은 초기 기본 플래그 값인 EMPTY_FLAG를 할당한다. 

 

 

            2️⃣  함수 만들기 

                   

                  진입 시도하여 락을 걸어주는 WriteLock()과 

                  진입 했을 때, 락을 풀어주는 WriteUnLock() 

                  그리고 마찬가지로 ReadLock()ReadUnLock()을 껍데기만 일단 만들어 두자.

 

 

    // 재귀적 락을 허용할 지(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;

    public void WriteLock()
    {
        ...
    }

    public void WriteUnlock()
    {
         ...
    }

    public void ReadLock()
    {
         ...
    }

    public void ReadUnlock()
    {
         ...
    }

}

 

 

              3️⃣  WriteLock 과 WriteUnLock 구현하기

 

                  👉 WriteLock 

                     1) Thread.CurrentThread.ManagedThreadId를 통해 현재 스레드 Id를 얻고,

                         WRITE_MASK와의 논리 연산을 통해 desire 값으로 초기화 한다. 

 

                     2) _flag의 값을 확인하면서 lock을 획득하고 있는 지 아닌지를 반복문을 돌면서 확인한다.

 

                     3) _flag 값이 EMPTY_FLAG와 동일하면 _flag값에 desire 값을 할당한다

 

                        (진입하여 lock을 획득한 다음, 반복문을 나온다. ❗ 이 때,  Interlocked의 compareExchange를 활용해 값을

                         비교하고 초기화 하는 작업이 한 번에 이루어 지도록 한다. )

 

                     4)  5천번 이상 시도를 했을 때는, Yield()를 통해 다른 스레드에게 점유를 넘긴다. 

 

 

                👉 WriteUnLock 

                      1) 쓰기가 종료되면 락을 풀어 주어야 한다.

                         _flag 값을 다시 초기값(EMPTY_FLAG == 0) 으로 바꾼다. 

 

 

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

            Thread.Yield();
        }
    }
}

public void WriteUnlock()
{
    Interlocked.Exchange(ref _flag, EMPTY_FLAG);
}

 

 

 

 

              4️⃣ ReadLock과 ReadUnlock 구현하기

 

                    1️⃣ Readlock

                    1) _flag와 READ_MASK 논리 연산 값을 expected에 할당한다. 

                       (읽을 수 있는 지 여부를 확인하기 위한 변수 설정)

 

                    2) _flag와 expected 값이 같으면, expected 값을 1 증가 (write 상태가 아님을 의미)

 

                    3) 최대 시도 횟수를 넘어가면 다른 스레드에게 Yield()를 통해 양보한다.

 

 

                    👉 ReadUnlock

                    1) 락을 풀어주기 위해 증가시켰던 값을 Interlocked 통해 감소시킨다 

 

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;

            //해당 방법은 스레드 동기화가 잘 이루어지지 않는 문제 발생 -> 한번에 처리하는 코드로 바꿔야 함 
            /*if((_flag & WRITE_MASK) == 0)
            {
                _flag = _flag + 1;
                return;
            }*/
        }

        Thread.Yield();
    }
}

public void ReadUnlock()
{
    Interlocked.Decrement(ref _flag); // 1을 줄임 
}

 

 

 

 

    👉   재귀적 락을 허용하는 경우 

 

             재귀적 락을 허용하는 경우란 다음 경우를 말한다. 

             동일 스레드 내에서(Write 스레드가) 하고 Write 하는 경우 Ok

             Write -> Read 하는 경우도 Ok

 

            하지만 동일 스레드 내에서 Read -> Write하는 것은 막는다.

            즉, 읽으면서 쓰는 경우는 막는 것이다.  

 

 

            1️⃣   WriteLock과 WriteUnlock의 달라지는 점

 

                      👉 Writelock

                    1) 현재 스레드 ID와 lock을 소유하고 있는 ID를 확인하여 이미 writelock 을 획득하고 있는 지 확인한다.

                        1-2) 획득하고 있으면, _writeCount 변수를 두어 값을 증가시킨다. (재귀적 허용)

                                소유하고 있지 않을 때는 기존과 같다.

 

                    👉 WriteUnlock

                     1) lock을 획득한 만큼(증가시킨 만큼) 풀어주어야 한다. _writeCount 의 값을 감소시킨다.

                     2) _writeCount가 0이 되었을 때, write을 unlock 시킨다. (초기 상태로 세팅한다.) 

 

 

            2️⃣   ReadLock과 ReadUnlock의 달라지는 점

 

                      1) 현재 스레드가 이미 writeLock을 획득하고 있는 지 확인한다.

                      2) 소유하고 있다면  _flag 값 자체를 증가시킨다. (_flag 값 자체가 ReadCount가 된다 - 공유 카운트) 

 

                     소유하고 있지 않을 때는 동일하며 ReadUnlock 또한 동일하다. 

 

 

최종 코드 

// 재귀적 락을 허용할 지(No)
// 재귀적 락을 허용할 지(YES) WriteLock -> WriteLock OK, WriteLock->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 desire = (Thread.CurrentThread.ManagedThreadId << 16) && WRITE_MASK;
    while (true)
    {
        for (int i = 0; i < MAX_SPIN_COUNT; i++)
        {
            if (Interlocked.CompareExchange(ref _flag, desire, EMPTY_FLAG) == EMPTY_FLAG)
            {
                _writeCount = 1;
                return;
            }


            /*//시도를 해서 성공하면 return 
            if (_flag == EMPTY_FLAG)
                _flag = desire;*/
        }

        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;
            //해당 방법은 스레드 동기화가 잘 이루어지지 않는 문제 발생 -> 한번에 처리하는 코드로 바꿔야 함 
            /*if((_flag & WRITE_MASK) == 0)
            {
                _flag = _flag + 1;
                return;
            }*/
        }

        Thread.Yield();
    }
}

public void ReadUnlock()
{
    Interlocked.Decrement(ref _flag); // 1을 줄임 
}

}

 

 

메인 실행문 

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.ReadLock();
            count--;
            _lock.ReadUnlock();
        }
    });

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

    Task.WaitAll(t1, t2);

    Console.WriteLine(count);

}

 

 

 


 

 

 ⭐ 정리(재귀적 허용일 때)

          WriteLock 메소드 :   스레드 Id를 확인하여 write 락을 소유하고 있는 지 확인한다. 

                                           다른 스레드는 현재 스레드가 소유권을 포기하기 전까지 대기 상태로 있어야 한다.

                                          (소유권은 한 스레드만 획득 가능)

                                         동일한 스레드가 소유권을 또 가지려고 할 때, 별도의 _writeCount 변수를 두어 이 값을 증가시킨다. 

 

          WriteUnlock 메소드 :  _writeCount 가 증가된 만큼 감소시키고 그 값이 0이 되면 락을 해제한다. 

 

          ReadLock 메소드 :  write을 소유하고 있지 않을 때, 다수의 스레드가 경합하여 공유 카운트를 올리게 한다.

                                         동일한 스레드가 소유하고 있을 때에도 공유 카운트를 증가시켜야 한다. 

 

          ReadUnlock 메소드 :  공유 카운트의 값을 감소시켜 락을 풀어준다. 

 

728x90
반응형
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
반응형