본문 바로가기
공부/네트워크

네트워크 공부 - 멀티스레딩(MultiThread 개념)

by 라이티아 2024. 10. 19.

순서

1. 프로그램과 프로세스

2. 스레드

3. 멀티스레드 프로그래밍은 언제 해야 할까?

4. 스레드 정체

5. 스레드를 다룰 때 주의 사항

6. 임계 영역과 뮤텍스

7. 교착 상태

8. 잠금 순서의 규칙

9. 병렬성과 시리얼 병목

10. 싱글스레드 게임 서버

11. 멀티스레드 게임 서버

12. 원자 조작

 

1.1 프로그램과 프로세스

프로그램

컴퓨터에서 실행되는 명령어 모음이 들어있는 데이터 덩어리

프로세스

프로그램이 활동을 하는 상태

로딩

프로그램에 있는 코드와 데이터를 프로세스 메모리로 불러들임

 

 

프로그램 = 코드 + 데이터 => (로딩) => 프로세스 = 코드 힙 데이터 스택

 

프로그램은 disk에 프로세스는 ram에 있음

 

멀티 프로세싱

프로세스가 여러 개 실행되고 있는것

 

 

1.2 스레드(Thread)

스레드와 프로세스의 차이점

1. 스레드는 한 프로세스 안에 여러 개가 있다

2. 스레드는 프로그램의 흐름

3. 한 프로세스 안에 있는 스레드는 프로세스 안에 있는 메모리 공간을 같이 사용할 수 있다

4. 스레드마다 스택을 가진다. 이는 각 스레드가 실행되는 함수의 로컬 변수들이 스레드마다 있다는 의미이다

 

정리

프로세스 안에 스레드가 존재하며

이때 스레드는 한 프로세스 안에 다수 존재가 가능하다

이 경우, 스레드들은 스텍은 제외한 코드, 힙, 데이터를 공유한다

스텍의 경우 공유시 처리 부하가 걸리기에 제외한다

 

싱글스레드 프로그램

동시에 하나만 실행되는 프로그램

 

싱글스레드 모델

싱글스레드로만 작동하도록 프로그램을 설계하고 구현하는것

 

멀티스레드 모델 or 멀티스레딩

여러 스레드가 동시에 여러 가지 일을 처리하게 하는것

 

 

1.3 멀티스레드 프로그래밍은 언제 해야할까?

멀티스레드 프로그래밍을 해야 하는 대표적인 상황들

1. 오래 걸리는 일 하나와 빨리 끝나는 일 여럿을 같이 해야 할 때

2. 어떤 긴 처리를 진행하는 동안 다른 짧은 일을 처리해야 할 때

3. 기기에 있는 cpu를 모두 활용해야 할 때

 

예시) 게임의 로딩

 

 

1.4 스레드 정체

두가지 일을 동시에 하라 라고 시켰을 때

컴퓨터는 왔다 갔다 하면서 일을 처리함

 

컨텍스트 스위치(context swich)

각 스레드를 실행하다 말고 다른 스레드를 마저 실행하는 과정

이 과정이 잦을경우 이동 시간으로 인해 처리 시간이 길어져서 비효율적임

 

스레드 개수가 cpu보다 많을시 컨텍스트 스위치는 무조건 발생함

스레드가 많더라도 Runnable인 스레드가 cpu보다 적다면 문제가 없음

 

문제사항

스레드 2개가 값 하나에 동시에 접근할 경우

using System;
using System.Threading;

class Program
{
    static int sharedValue = 0;

    static void Main()
    {
        // 두 개의 스레드 생성
        Thread thread1 = new Thread(IncrementValue);
        Thread thread2 = new Thread(IncrementValue);

        // 두 스레드 시작
        thread1.Start();
        thread2.Start();

        // 두 스레드가 끝날 때까지 대기
        thread1.Join();
        thread2.Join();

        // 최종 값 출력
        Console.WriteLine("최종 값: " + sharedValue);
    }

    static void IncrementValue()
    {
        for (int i = 0; i < 100000; i++)
        {
            // sharedValue를 증가
            sharedValue++;
        }
    }
}

결과값이 100059로 이상하게 나온다

 

이는, 2개의 스레드가 경쟁상태가 되었기 때문

 

 

1.5 스레드를 다룰 때 주의 사항

원자성

두 변수 값 모두 바꾸던지, 모두 안 바꾸던지

일관성

두 변수 항상 일관된 상태 유지

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

class Program
{
    static List<int> primes = new List<int>(); // 공유되는 소수 리스트
    static object _lock = new object(); // 스레드 동기화를 위한 락 오브젝트

    static void Main()
    {
        // 스레드를 저장할 배열
        Thread[] threads = new Thread[4];

        // 4개의 스레드 생성 (각각 다른 범위에서 소수 찾기)
        for (int i = 0; i < threads.Length; i++)
        {
            int start = i * 250000 + 1; // 각 스레드가 처리할 시작 숫자
            int end = (i + 1) * 250000; // 각 스레드가 처리할 끝 숫자

            threads[i] = new Thread(() => FindPrimesInRange(start, end));
            threads[i].Start();
        }

        // 모든 스레드가 작업을 완료할 때까지 기다림
        foreach (var thread in threads)
        {
            thread.Join();
        }

        // 최종 소수 개수 출력
        Console.WriteLine("1부터 1,000,000까지의 소수 개수: " + primes.Count);
    }

    // 특정 범위에서 소수를 찾는 함수
    static void FindPrimesInRange(int start, int end)
    {
        for (int num = start; num <= end; num++)
        {
            if (IsPrime(num))
            {
                primes.Add(num);
            }
        }
    }

    // 소수 판별 함수
    static bool IsPrime(int number)
    {
        if (number < 2)
        {
            return false;
        }

        for (int i = 2; i * i <= number; i++)
        {
            if (number % i == 0)
            {
                return false;
            }
        }

        return true;
    }
}

결과값이 이상하게 나온다

 

왜냐하면 list에 add할때 스레드 경쟁 상태가 되기 때문

 

 

두 스레드가 동시에 List에 add를 하면 여러 스레드가 list 변수들을 변경, 그러면 두 변수중 하나만 변경된 상태에서 다른 스레드가 그래도 배열을 엑세스함, 이 과정에서 배열을 가리키는 포인터 변수는 엉뚱한 값, 예로 이미 힙에서 해제된 메모리를 잠시 가리킬 수도 있음

 

원자성

두 멤버 변수를 건드리는 동안에 다른 스레드가 절대 건드리지 못하게 해야함

일관성

list의 두 변수는 항상 일관성 있는 상태를 유지해야 함

 

이에 따라서 동기화 기법이 필요함

원자성과 일관성을 유지하는 특수한 조치, 임계영역, 뮤텍스(상호 배제), 잠금(lock)기법이 사용된다

 

임계영역?

여러 스레드가 공유 리소스에 동시에 엑세스 할 수 있는 가능성이 있는 코드 영역을 말함

 

 

1.6 임계 영역과 뮤텍스

경쟁상태를 해결하는 여러 방법중 하나

스레드에서 어떤 정보를 사용하고 있는 동안 다른 스레드는 정보를 건드리지 못하게 한다

 

뮤텍스(Mutax - Mutual exclusion)

상호배제의 줄임말

 

사용법

1. x, y를 보호하는 뮤텍스를 제작

2. 스레드는 x, y를 건들기 전에 뮤텍스에게 사용권을 얻으려 함

3. 스레드가 x, y에 엑세스

4. 엑세스 종료시 뮤텍스에게 사용권을 놓겠다 선언

 

    static void FindPrimesInRange(int start, int end)
    {
        for (int num = start; num <= end; num++)
        {
            if (IsPrime(num))
            {
                // lock을 사용하지 않으면 스레드 충돌이 발생할 수 있음
                lock (_lock) // 동기화를 위한 lock (충돌 방지)
                {
                    primes.Add(num);
                }
            }
        }
    }

 

소수를 구할때 lock을 넣는 것으로 뮤텍스를 사용할 수 있다

(monitor라는 살짝 다른 개념이지만, 현 수준에서는 같은 기능을 한다)

 

뮤텍스를 너무 잘게 나누면

오히려 프로그램 성능이 떨어짐 => 뮤텍스를 엑세스 하는 과정이 무겁기 때문

프로그램이 매우 복잡해짐, 특히 교착 상태 문제가 쉽게 발생

 

 

1.7 교착 상태

두 스레드가 서로를 기다리는 상황

스레드 1은 2가 하던 일이 끝날때 까지 대기

스레드 2는 1이 하던 일이 끝나기를 기다림

= 영원히 대기를 하게 됨

 

using System;
using System.Threading;

class Program
{
    // 두 개의 락 오브젝트
    static object lock1 = new object();
    static object lock2 = new object();

    static void Main()
    {
        // 첫 번째 스레드
        Thread thread1 = new Thread(Thread1Work);
        // 두 번째 스레드
        Thread thread2 = new Thread(Thread2Work);

        // 두 스레드 실행
        thread1.Start();
        thread2.Start();

        // 스레드 종료 대기
        thread1.Join();
        thread2.Join();
    }

    // 첫 번째 스레드 작업: lock1을 잠그고, lock2를 잠그려고 시도
    static void Thread1Work()
    {
        lock (lock1)
        {
            Console.WriteLine("Thread 1: lock1 획득, lock2를 기다림...");
            Thread.Sleep(100); // 잠시 대기하여 교착상태를 유발
            lock (lock2)
            {
                Console.WriteLine("Thread 1: lock2 획득!");
            }
        }
    }

    // 두 번째 스레드 작업: lock2를 잠그고, lock1을 잠그려고 시도
    static void Thread2Work()
    {
        lock (lock2)
        {
            Console.WriteLine("Thread 2: lock2 획득, lock1을 기다림...");
            Thread.Sleep(100); // 잠시 대기하여 교착상태를 유발
            lock (lock1)
            {
                Console.WriteLine("Thread 2: lock1 획득!");
            }
        }
    }
}

 

게임 서버에 교착 상태가 되면 발생하는 현상

CPU 사용량이 현저히 낮거나 0%, 동시접속자 수와 상관없음

클라이언트가 서버를 이용할 수 없음, 로그인 불가, 요청시 피드백 없음 등등

 

 

1.8 잠금 순서의 규칙

여러 뮤텍스를 사용할 때 교착 상태를 예방할려면

각 뮤텍스의 잠금 순서를 먼저 그래프로 그려둔다

그리고 잠금을 할 때는 잠금 순서 그래프를 보면서 거꾸로 잠근 것이 없는지 체크 해야 한다

 

lock

A -> B -> C

unlock

C -> B -> A

 

이런 규칙을 정했을시

 

B -> A

A -> B

를 할 시 교착상태가 일어난다

 

C -> A 또한 동일하다

 

재귀 뮤텍스

한 스레드가 뮤텍스를 여러번 반복해서 잠그는 것을 원활하게 처리

lock

A -> A

unlock

A -> A

두번 잠그고 두번 푼다

한번만 풀시 문제가 된다

 

 

1.9 병렬성과 시리얼 병목

병렬성과 시리얼 병목

병렬성 - 여러 CPU가 각 스레드의 연산을 실행하여 동시 처리량을 올리는것

시리얼 병목 - 병렬로 실행되게 프로그램을 만들었는데, 정작 한 CPU만 연산을 수행하는 현상

 

암달의 저주 줄이기

암달의 저주

CPU개수가 많을수록 총 처리 효율성이 떨어지는 현상 

 

암달의 저주를 줄이려면 시리얼 병목이 발생하는 구간을 최소화 하여야 한다

 

 

1.10 싱글 스레드 게임 서버

싱글스레드 서버를 구동하는 경우 CPU의 개수 만큼 프로세스를 띄우는 것이 일반적

1. 방 개수만큼 스레드나 프로세스가 있으면 스레드나 프로세스 간 컨텍스트 스위치의 횟수가 증가

2. 따라서 같은 동시 접속자를 처리하는 서버라고 하더라도 실제로 처리할 수 있는 동시 접속자 수를 크게 떨어뜨림

 

 

1.11 멀티 스레드 게임 서버

멀티스레드로 서버를 개발하는 경우

1. 서버 프로세스를 많이 띄우기 곤란할 때, 프로세스당 로딩해야 하는 게임 정보가 클때

2. 서버 한 대의 프로세스가 여러 CPU의 연산량을 동원해야 할 만큼 많은 연산을 할때

3. 코루틴이나 비동기 함수를 쓸 수 없고 디바이스 타임이 발생할때

4. 서버 인스턴스를 서버 기기당 하나만 두어야 할 때

5. 서로 다른 방이 같은 메모리 공간을 엑세스 해야 할때

 

 

1.12 원자 조작

뮤텍스나 임계 영역 잠금 없이도 여러 스레드가 안전하게 접근할 수 있는 것을 의미

원자 조작은 하드웨어 기능이며, 대부분의 컴파일러는 원자 조작 기능을 쓸 수 있게 함

원자 조작은 32bit나 64bit의 변수 타입에 여러 스레드가 접근 할 때 한 스레드씩만 처리됨을 보장