에코 서버란 클라이언트에서 수신 된 데이터를 그대로 다시 클라이언트에게 되돌려 보내는 서버를 의미한다.

Iterative란 반복적이라는 의미로 각 클라이언트의 연결 요청을 순차적으로 처리하는 서버를 의미한다.

이 서버는 한 번에 하나의 클라이언트 요청만 처리하며, 하나의 연결이 완료된 후 다음 연결을 처리한다.

 

// Server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
void ErrorHandling(char* message);

int main(int argc, char* argv[]) {
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	char message[BUF_SIZE];
	int strLen, i;

	SOCKADDR_IN servAddr, clntAddr;
	int clntAddrSize;
	

	if (argc != 2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
		ErrorHandling("WSAStartup() error!");
	}

	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET) {
		ErrorHandling("socket() error!");
	}

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) {
		ErrorHandling("bind() error!");
	}

	if (listen(hServSock, 5) == SOCKET_ERROR) {
		ErrorHandling("listen() error!");
	}

	clntAddrSize = sizeof(clntAddr);

	// 연결 요청 대기 큐의 크기만큼 반복
	for (i = 0; i < 5; i++) {
		
		hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &clntAddrSize);
		if (hClntSock == -1) {
			ErrorHandling("accept() error!");
		}
        // 연결 되었다면 몇 번째 클라이언트인지 출력
		else {
			printf("Connected client %d \n", i + 1);
		}
		
        // 클라이언트한테 데이터를 받으면
		while ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) != 0) {
        	// 그대로 돌려주기
			send(hClntSock, message, strLen, 0);
		}

		closesocket(hClntSock);
	}
	
	closesocket(hServSock);
	WSACleanup();

	return 0;
}

void ErrorHandling(char* message) {
	fputs(message, stderr);
	fputs("\n", stderr);
	exit(1);
}

 

// Client

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 1024
#define _WINSOCK_DEPRECATED_NO_WARNINGS
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	char message[BUF_SIZE];
	int strLen;
	SOCKADDR_IN servAddr;

	if (argc != 3) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
		ErrorHandling("WSAStartup() error!");
	}

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET) {
		ErrorHandling("socket() error!");
	}

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) {
		ErrorHandling("connect() error!");
	}
	else {
		printf("Connected.........");
	}

	while (1) {
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		
        // 입력 값이 q or Q이면 종료
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
			break;
		}
		
        // 입력 값 보내기
		send(hSocket, message, strlen(message), 0);
		strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
		message[strLen] = 0;
        // 서버로부터 받은 값 출력
		printf("Message from server: %s", message);
	}

	closesocket(hSocket);
	WSACleanup();

	return 0;
}

void ErrorHandling(char* message) {
	fputs(message, stderr);
	fputs("\n", stderr);
	exit(1);
}

 

 

2개의 클라이언트를 실행하였다.

2개의 클라이언트를 실행 했으나 서버에는 Connected client1만 뜬 상태에서 client1과 1대1 통신이 진행되었다.

client1에서 입력한 값을 서버에서 그대로 돌려주었고 q를 눌러 연결을 끊었을 때 서버에서 Connected clinet2가 뜨며 client2와 연결이 되었다.

멈춰있던 client2에서도 입력이 가능해졌고 입력값을 그대로 돌려받았다.

 

 

위의 Iterative 에코 클라이언트에서  문제가 생길 수 있는 부분이 있다.

while (1) {
    fputs("Input message(Q to quit): ", stdout);
    fgets(message, BUF_SIZE, stdin);

    if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
        break;
    }

    send(hSocket, message, strlen(message), 0);
    strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
    message[strLen] = 0;
    printf("Message from server: %s", message);
}

해당 부분에서 입력 받은 데이터를 send를 통해 통째로 서버에 보내고 있다.

다음줄에서는 recv를 통해 서버에서 데이터를 통째로 받고 있다.

 

서버에서 데이터를 다 전달하지 못하였는데 recv를 해버리는 경우 문제가 발생할 수 있다.

 

while (1) {
    fputs("Input message(Q to quit): ", stdout);
    fgets(message, BUF_SIZE, stdin);

    if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
        break;
    }

    strLen = send(hSocket, message, strlen(message), 0);
    recvLen = 0;

    while (recvLen < strLen) {
        recvCnt = recv(hSocket, message, BUF_SIZE - 1, 0);
        if (recvCnt == -1) {
            ErrorHandling("recv() error!");
        }
        recvLen += recvCnt;
    }

    message[strLen] = 0;
    printf("Message from server: %s", message);
}

위와 같이 바꾸어 주면 해당 문제를 해결할 수 있다.

strLen에 send 해주는 message의 길이를 저장해 둔 후 recv한 데이터의 길이가 strLen보다 커질 때 while을 빠져나와 출력을 해주면 된다.

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

소켓 프로그래밍 - TCP 소켓  (0) 2023.09.10
ReaderWriterLock 구현  (0) 2023.08.05
ReaderWriterLock  (0) 2023.08.04
AutoResetEvent  (0) 2023.08.03
SpinLock  (0) 2023.08.02
// Server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
void ErrorHandling(char* message);

int main(int argc, char* argv[]) {
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAddr, clntAddr;

	int szClntAddr;
	char message[] = "Hello World!";

	// argc가 2가 아니라면 즉, 포트번호가 설정이 안되었다면 종료
	if (argc != 2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
    
	// Winsock 라이브러리의 초기화를 수행
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
		ErrorHandling("WSAStartup() error!");
	}
    
	// 소켓을 생성하며, 이 소켓은 서버와 클라이언트 간의 연결에 사용
	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET) {
		ErrorHandling("socket() error!");
	}
    
	// servAddr 구조체를 0으로 초기화
	// INADDR_ANY를 사용하여 소켓이 동작하는 컴퓨터의 IP주소를 자동으로 할당
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(atoi(argv[1]));

	// 생성한 소켓에 주소와 포트 번호를 바인딩
	if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) {
		ErrorHandling("bind() error!");
	}

	// 클라이언의 연결 요청을 기다리기 시작
	// 연결 요청 대기 큐의 크기를 5로 설정하여 클라이언트의 연결 요청을 5개까지 대기 가능
	if (listen(hServSock, 5) == SOCKET_ERROR) {
		ErrorHandling("listen() error!");
	}

	// 클라이언트의 연결 요청을 수락하고, 연결된 클라이언트와의 통신을 위한 새로운 소켓을 반환
	szClntAddr = sizeof(clntAddr);
	hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
	if (hClntSock == INVALID_SOCKET) {
		ErrorHandling("accept() error!");
	}

	// 연결된 클라이언트에게 "Hello World!" 메시지를 전송
	send(hClntSock, message, sizeof(message), 0);
    
	// 더 이상 필요하지 않은 소켓을 닫기
	closesocket(hClntSock);
	closesocket(hServSock);
    
	// Winsock 라이브러리의 사용을 종료하고, 할당된 리소스를 해제
	WSACleanup();

	return 0;
}

void ErrorHandling(char* message) {
	fputs(message, stderr);
	fputs("\n", stderr);
	exit(1);
}

 

// Client

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30];
	int strLen = 0;
	int idx = 0, readLen = 0;
	
    // argc가 3이 아니라면 즉, IP와 포트번호가 설정이 안되었다면 종료
	if (argc != 3) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
		ErrorHandling("WSAStartup() error!");
	}

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET) {
		ErrorHandling("socket() error!");
	}

	// 사용자가 제공한 IP주소, 포트번호를 알맞는 형식으로 변환
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

	// 위에서 설정한 서버의 주소와 포트번호에 연결 시도
	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) {
		ErrorHandling("connect() error!");
	}

	// 서버로부터 데이터를 1바이트씩 수신
	while (readLen = recv(hSocket, &message[idx++], 1, 0)) {
		if (readLen == -1) {
			ErrorHandling("read() error!");
		}

		strLen += readLen;
	}

	// 서버로부터 받은 메세지와 길이를 출력
	printf("Message from server: %s \n", message);
	printf("Function read call count: %d", strLen);

	closesocket(hSocket);
	WSACleanup();

	return 0;
}

void ErrorHandling(char* message) {
	fputs(message, stderr);
	fputs("\n", stderr);
	exit(1);
}

포트번호를 9190을 설정한 후 실행 결과

서버에서 설정한 Hello World! 메세지를 클라이언트에서 수신했다.

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

소켓 프로그래밍 - Iterative 에코 서버  (0) 2023.09.10
ReaderWriterLock 구현  (0) 2023.08.05
ReaderWriterLock  (0) 2023.08.04
AutoResetEvent  (0) 2023.08.03
SpinLock  (0) 2023.08.02


 

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

+ Recent posts