게임프로그래밍/Unity_C#

[MMORPG 게임 개발(C#, Unity)] Part 4. 멀티스레드 프로그래밍

shine94 2025. 5. 26. 17:14

* 해당 글은 게임 프로그래머 입문 올인원 강의를 보고 정리한 글입니다

   https://www.inflearn.com/roadmaps/355#introduce

 

MMORPG 게임 개발, 켠김에 끝판왕까지! (유니티 + C#) 로드맵 - 인프런

C#, Unity 스킬을 학습할 수 있는 게임 개발 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

 

 

 

* Thread - 가장 기본적인 스레드

   직접 쓰레드를 생성하고 실행

   직접 OS에 접근(OS 커널 스레드 직접 생성)

   기본적으로 ForeGround 방식이며, IsBackground 옵션을 통해 BackGround 설정이 가능

   ThreadPool과는 연관이 없음

 

* ThreadPool - 재사용 가능한 스레드 집합

   .NET 런타임이 관리하는 제한된 스레드풀 구조

   생성된 스레드를 재사용

   빠르고 가벼우며, 반복 작업에 적합

   콜백 기반으로 실행되며, 비동기 흐름 자체는 아님(await 사용 불가)과는 다름

 

* Task - 비동기 기반의 현대적인 멀티스레딩 방식

   .NET에서 제공하는 고수준 비동기 작업 처리 모델

   작업 단위를 객체(Task)로 표현하며, 내부적으로는 ThreadPool 사용

   await 키워드와 함께 사용해 비동기 흐름을 직관적으로 표현 가능

   비동기 흐름을 자연스럽게 구성할 수 있어 실무에서 가장 많이 사용함

 

   (+) TaskCreationOptions.LongRunning → ThreadPool 안 씀

         ㄴ ThreadPool은 짧고 빈번한 작업 처리에 최적화(Long Blocking 작업에 부적합)

         ㄴ ThreadPool에서 긴 작업이 돌면 다른 짧은 작업들이 대기하다 풀 먹통 현상(스레드 고갈로 인한 blocking) 발생
         ㄴ [LongRunning 옵션] "오래 걸릴 거야, 독립 스레드 줘"란 의미

 

* 정리하면

항목 Thread ThreadPool Task
생성 방식 new Thread() ThreadPool.QueueUserWorkItem() Task.Run()
스레드 수 제한 ❌ 없음
(무제한 생성 가능, OS 한도까지)
(단, 과도한 생성시 성능 저하 발생)
✅ 있음
(MinThreads, MaxThreads)
✅ ThreadPool 기반
(MinThreads, MaxThreads)
관리 수동 관리 자동 관리
(.NET 스케줄링 기반)
자동 관리
(.NET 스케줄링 기반)
+
비동기 지원

 

https://sam0308.tistory.com/91

 

[C#] Thread, ThreadPool, Task

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다. ※ 해당 포스팅은 .Net 7.0 버전을 기준으로 작성되었습니다. 이번 포스팅에서는 C#에서 스레드를

sam0308.tistory.com

https://blog.naver.com/dlstngm/222510051731

 

[C#] 서버, 쓰레드[Thread, ThreadPool, Task]

일반적으로 C#은 CLR (Common Language Runtime)이라는 가상머신을 이용합니다. CLR은 마이크로...

blog.naver.com

 

* 릴리즈 빌드 디버깅 경고 처리

   릴리즈 빌드를 디버깅할 경우, 아래와 같은 경고창이 뜰 수 있음

   이때, [내 코드만] 사용 안 함 및 계속 선택

 

* 강사님의 최적화 관련 예제는 과거에는 실제로 문제가 발생하는 장면을 눈으로 확인할 수 있었지만,

   최신 비주얼 스튜디오 2022 기준으로는 컴파일러가 개선되어 같은 문제가 재현되지 않는다

   하지만 멀티스레드 환경에서는 여전히 주의가 필요하다

   컴파일러의 최적화는 단일 스레드 기준으로 수행되므로,

   변수의 값을 메모리에 즉시 반영하지 않거나, 실행 순서를 변경함으로써

   예상치 못한 흐름과 디버깅이 어려운 문제를 유발할 수 있다

 

*  C++과 C#의 volatile 차이

   C++

   컴파일러에게 해당 변수의 값을 레지스터 등에 캐시하지 말고, 항상 메모리에서 읽으라고 지시한다

   ㄴ 컴파일러 수준의 최적화만 방지할 뿐, CPU나 메모리 등 하드웨어 수준에서의 멀티스레드 동기화까지는 보장하지 않는다

 

   (+) 멀티스레드 환경에서 동기화 용도로는 volatile만으로는 부족하고,

         보통 `std::atomic`이나 `mutex`와 같은 별도의 동기화 수단이 필요하다

 

   C#

   C++의 의미에 더해, .NET 메모리에서 일정 수준의 가시성 보장(memory visibility)을 제공한다

   ㄴ 다른 스레드에서 변경된 값을 즉시 읽을 수 있게 해주는 효과가 있다

   ㄴ 메모리 가시성(memory visibility)

        한 스레드의 메모리 변경이 다른 스레드에게 언제, 어떤 순서로 보이는지를 제어하는 개념

 

   (+) 완벽한 동기화는 아니며,

         상황에 따라 `lock`, `Interlocked`, `Thread.MemoryBarrier()` 등의 추가 동기화 기법이 필요할 수 있다

 

* 정리하면

   C++은 컴파일러 최적화는 차단하지만 스레드 간 동기화를 보장하지 않는다

   C#은 메모리 가시성은 보장하지만, 동기화를 완전히 보장하지는 아니다

 

* 캐시

   CPU와 메모리 사이의 임시 저장 장치

   자주 사용하는 데이터를 빠르게 접근하기 위해 저장

 

* 지역성의 원리(Locality of Reference)

   프로그램은 특정 데이터나 명령어를 반복적으로 사용하고, 근처의 메모리 영역도 자주 접근하는 경향이 있음

   이러한 지역성의 특성을 활용하면 캐시는 효율적으로 동작함

 

   Temporal Locality(시간적 지역성)

   최근에 접근한 데이터는 다시 접근할 가능성이 높음

   (예) 루프 안에서 같은 변수 반복 사용

 

   Spatial Locality(공간적 지역성)

   가까운 메모리 주소에 접근할 가능성이 높음

   (예) 배열을 순차적으로 접근

 

* 레지스터

   CPU 내부에 위치한 매우 빠른 임시 저장 장치

   연산에 필요한 값을 임시로 저장하여 처리 속도를 높임

 

* 메모리 배리어(Memory Barrier)

   CPU의 명령어 재배치(Out-of-Order Execution) 억제

   메모리 가시성(memory visibility) 보장

 

   Store Buffer와 Load Buffer 같은 중간 저장소로 인해

   CPU가 메모리에 즉시 반영하지 않거나 최신 값을 읽지 못할 수도 있으므로,

   메모리 베리어를 통해 직접 메모리 접근을 강제하고 재배치 최적화를 방지한다

 

   Full Memory Barrier(전체 배리어)

   Store, Load 둘 다 순서 보장

   [어셈블리] MFENCE

   [C#] Thread.MemoryBarrier()

 

   Store Memory Barrier(쓰기 배리어)

   Store 순서 보장

   [어셈블리] SFENCE

   [C#] 직접 제공 없음(일부 상황에서는 volatile, Interlocked로 유사 동작 가능)

 

   Load Memory Barrier(읽기 배리어)

   Load 순서 보장

   [어셈블리] LFENCE

   [C#] 없음(일부 상황에서는 volatile로 유사 동작 가능)

 

   C#에서 개별 Load/Store 메모리 배리어를 직접 제공하지 않으며,

   volatile은 컴파일러 및 JIT 재배치만 제한할 뿐, 하드웨어 수준 배리어와 다르다

구분 volatile (소프트웨어 배리어) 하드웨어 메모리 배리어 (MFENCE, LFENCE, SFENCE)
동작 위치 컴파일러 & JIT 수준 CPU 하드웨어 내부
목적 명령어 재배치 억제, 최적화 방지 CPU 내부 버퍼/캐시 정리 및 재배치 억제
재배치 범위 소스 코드 → IL → JIT 간의 재배치만 방지 CPU의 out-of-order 실행 + Store/Load 버퍼 재배치 방지
가시성 보장 제한적
(같은 CPU 내에서만 완전 보장 가능)
다른 CPU 코어와도 메모리 일관성 보장
실제 적용 예 C#의 volatile, Java volatile MFENCE, SFENCE, LFENCE

 

   (+) volatile은 일부 플랫폼(예: ARM, x86)에서 부분적인 memory barrier를 제공하지만,

         하드웨어 레벨의 명시적 barrier(MFENCE 등)와는 다르다

 

   (+) 정리하면

         volatile은 컴파일러에게 "최적화하지 말고, 항상 메모리에서 접근해줘~"라고 청유하는 것이고,

         하드웨어 메모리 베리어는 CPU에게 "지금 당장 메모리에 써! 순서도 절대 바꾸지 마!"라고 강하게 명령하는 것이다

 

* 경합 조건(Race Condition)

   둘 이상의 스레드가 동시에 같은 자원에 접근하고, 그 접근 순서에 따라 실행 결과가 달라지는 상황

   동기화 없이 공유 자원에 접근하면, 예상하지 못한 값이 저장되거나, 작업이 중복되거나 누락될 수 있다

 

* 원자성

   https://shine94.tistory.com/384

 

[ETC] 메모리 원자성과 DB 원자성

* 메모리 원자성(Atomicity)   데이터의 읽기 또는 쓰기 작업이 분할되지 않고 한 번에 처리되는 성질을 의미한다 * 캐시 라인(Cache Line)과 메모리 원자성의 관계   캐시 라인이란?   CPU가 메모리

shine94.tistory.com

 

* ref 키워드

   이미 있는 값을 수정하기 위해 사용

   초기화된 값을 참조로 전달

* out 키워드

   새 값을 돌려주려고 사용

   메서드에서 값을 반환하기 위해 참조로 전달

 

* 크리티컬 섹션(Critical Section) 기반의 동기화 - 유저모드 동기화

   동시에 여러 스레드가 접근하면 안 되는 공유 자원 접근 영역

 

   [C#] lock 키워드, Monitor 클래스의 Enter와 Exit

   [C언어] EnterCriticalSection, LeaveCriticalSection

 

* 인터락 함수 기반의 동기화 - 유저모드 동기화

   크리티컬 섹션 기반의 동기화 용도로 특화된 함수

   여러 스레드가 동시에 접근해도 원자성(atomic)으로 연산할 수 있게 해주는

   C언어에서는 함수로, C#에서는 유틸리티 클래스 메서드로 제공

 

   [C#] Interlocked 클래스에 Increment, Decrement 메서드가 있음

   [C언어] InterlockedIncrement, InterlackedDecrement

   

* 뮤텍스(Mutex) 기반의 동기화 - 커널모드 동기화

   값이 0 또는 1인 바이너리 세마포어(binary semaphore)이므로, 단일 자원에 대한 상호 배제를 구현한다

 

   [C#] Mutex는 생성하면서 바로 락을 획득하거나, 나중에 WaitOne으로 획득할 수도 있다

   [C언어] CreateMutex로 핸들을 생성한 뒤, _beginthreadex 함수로 락을 획득할 수 있다

 

* 커널 객체 기반 동기화는 비용이 크지만, 유저모드 방식보다 더 강력한 제어와 프로세스 간 공유가 가능하다

 

https://shine94.tistory.com/400

 

[뇌를 자극하는 윈도우즈 시스템 프로그래밍] 13장, 14장. 쓰레드 동기화 기법 1, 쓰레드 동기화 기

* 해당 글은 윤성우의 뇌를 자극하는 윈도우즈 시스템 프로그래밍 도서를 읽고 정리한 글입니다   https://product.kyobobook.co.kr/detail/S000001223395 뇌를 자극하는 윈도우즈 시스템 프로그래밍 | 윤성우

shine94.tistory.com

 

* 데드락(Deadlock)

   두 개 이상의 스레드가 서로 상대 자원을 기다리면서 영원히 멈춰있는 상태

 

* 데드락 발생 조건(4가지 모두 만족 필수)

   (1) 상호 배제

         자원은 한 번에 하나의 스레드만 점유 가능

   (2) 점유와 대기

         이미 자원 점유한 상태로 다른 자원 기다림

   (3) 비선점

         자원을 강제로 뺏을 수 없음

   (4) 순환 대기

         스레드 A → B → A 순환 구조

 

* 핑(Wrapping)

   어떤 기능, 객체 또는 메서드를 다른 구조(또는 함수) 안에 감싸는 것

   직접 접근하지 않고, 간접적으로 사용하도록 만드는 것

 

* 스핀락(SpinLock)

   락이 풀릴 때까지 루프 돌면서 기다리는 동기화 기법

   락 점유 시간이 매우 짧을 것으로 예상될 때 유리한 고성능 락 기법

   Interlocked.Exchange, Interlocked.CompareExchange를 사용하여 구현 가능

 

https://hogwart-scholars.tistory.com/entry/OS-Spin-Lock-%EC%8A%A4%ED%95%80%EB%9D%BD%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

[OS] Spin Lock (스핀락)에 대해 알아보자

개요 때는... 2023년 5월 24일. MeetCoder 10기 첫번째 밑업을 진행하던 중이었다. 민철님의 '분산락을 이용한 동시성 이슈 해결' 발표가 진행되던 중 채팅창에 요런 질문이 나왔다. 스핀락...? 스핀락이

hogwart-scholars.tistory.com

https://velog.io/@yarogono/C-SpinLock%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

[C#] SpinLock을 알아보자

어려운 개념인 Lock을 간단한 SpinLock 예제 코드와 함께 정리해봤습니다. SpinLock 코드를 직접 타이핑하고 실행하면서 멀티 스레드 환경에 대해 기초적인 부분을 이해할 수 있었습니다.

velog.io

 

* 문맥 교환(Context Switching)

   운영체제가 현재 실행중인 프로세스(또는 스레드)의 상태(Context)를 저장하고,

   다른 프로세스(또는 스레드)의 상태를 복원하여 CPU 제어권을 넘기는 작업

   오버헤드가 크기 때문에 빈번하게 발생하면 성능 저하가 발생할 수 있음

 

* 오버헤드는 리소스를 추가로 소모하게 되는 행위나 비용이다

 

* AutoResetEvent - 커널모드 동기화, 이벤트 기반 동기화

   .NET의 스레드 간 동기화를 위한 클래스 중 하나로

   한 번의 신호로 하나의 스레드만 깨어나게 하는 자동 리셋 이벤트

   대기 중인 스레드가 없을 경우 신호는 손실됨

항목 AutoResetEvent ManualResetEvent
신호 후 자동 리셋 ✅ 예 ❌ 아니요 (직접 Reset() 호출 필요)
한 스레드만 통과 ✅ 예 ❌ 모든 대기 중인 스레드 통과
사용 용도 단일 소비자 알림 브로드캐스트용 알림

   [C언어] CreateEvent 함수에서 bManualReset 값이 TRUE면 Manual, FALSE면 Auto 동작이 된다

 

* ReaderWirteLock, ReaderWriterLockSlim - 읽기/쓰기 동기화 락

   다중 스레드 환경에서 읽기 작업은 동시에 허용하고, 쓰기 작업은 단독으로 허용하는 락

 

   읽기를 병렬로 처리함으로 성능이 향상되지만, 교착상태(deadlock) 발생 가능성에 유의해야 한다

   쓰기를 보호를 강화함으로 락 경합(lock contention)  또는, (race condition)이 심하면 성능 저하 가능

 

* Thread Local Storeage (TLS) - 스레드 전용 데이터 저장소

   각 스레드가 자신만의 고유한 데이터를 저장할 수 있도록 하는 기능