콘텐츠로 이동

동시성 모델 비교

현대 게임은 렌더링, 물리, AI, 네트워크를 동시에 처리해야 합니다. 동시성 모델을 잘못 선택하면 교착 상태(Deadlock), 경쟁 조건(Race Condition), 불필요한 컨텍스트 스위치가 발생합니다. 네 가지 핵심 동시성 모델의 원리와 적합한 사용 시나리오를 비교합니다.


OS가 스케줄링하는 가장 기본적인 동시성 단위입니다.

#include <thread>
#include <mutex>
#include <vector>
std::mutex g_mutex;
int g_score = 0;
void UpdateScore(int delta)
{
std::lock_guard<std::mutex> lock(g_mutex);
g_score += delta;
}
// 스레드 생성 비용: ~수십 마이크로초
// 스택 크기: 기본 1~8 MB
// 컨텍스트 스위치: ~수 마이크로초
void RunThreadExample()
{
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++)
threads.emplace_back(UpdateScore, 1);
for (auto& t : threads) t.join();
}

적합한 경우: CPU 바운드 병렬 연산, 물리 시뮬레이션 분할, 에셋 로딩

주의: 스레드 수 > CPU 코어 수이면 컨텍스트 스위치 오버헤드가 증가합니다.


실행을 중단하고 나중에 재개하는 협력형 멀티태스킹입니다. 컨텍스트 스위치 없이 단일 스레드에서 동시성을 표현합니다.

#include <coroutine>
#include <iostream>
// 단순 제너레이터 코루틴
struct Generator
{
struct promise_type
{
int current_value;
Generator get_return_object() { return Generator{this}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int v)
{
current_value = v;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
explicit Generator(promise_type* p)
: handle(std::coroutine_handle<promise_type>::from_promise(*p)) {}
~Generator() { if (handle) handle.destroy(); }
bool Next() { handle.resume(); return !handle.done(); }
int Value() { return handle.promise().current_value; }
};
Generator CountDown(int from)
{
for (int i = from; i >= 0; i--)
co_yield i;
}
// 사용
auto gen = CountDown(5);
while (gen.Next())
std::cout << gen.Value() << ' '; // 5 4 3 2 1 0
// Unity 코루틴: 단일 메인 스레드에서 프레임 분산 실행
IEnumerator LoadLevelCoroutine(string sceneName)
{
// 로딩 UI 표시
loadingScreen.SetActive(true);
// 비동기 씬 로드 (완료까지 프레임 양보)
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
op.allowSceneActivation = false;
while (op.progress < 0.9f)
{
loadingBar.value = op.progress;
yield return null; // 다음 프레임까지 대기
}
loadingBar.value = 1f;
yield return new WaitForSeconds(0.5f); // 0.5초 대기
op.allowSceneActivation = true;
}

적합한 경우: 프레임 분산 처리, 순차적 타이밍 로직, 단순 비동기 흐름


I/O 대기 중 스레드를 반환해 적은 스레드로 높은 처리량을 달성합니다.

// C# async/await — 게임 서버 예시
public class GameServer
{
private readonly HttpClient _http = new();
// 스레드를 블로킹하지 않고 I/O 대기
public async Task<PlayerData> FetchPlayerDataAsync(int playerId)
{
// 대기 중 스레드 반환 → 다른 요청 처리 가능
string json = await _http.GetStringAsync(
$"https://api.game.com/players/{playerId}");
return JsonSerializer.Deserialize<PlayerData>(json)!;
}
// 병렬 비동기 작업
public async Task<(PlayerData, LeaderboardData)> LoadGameDataAsync(int playerId)
{
// 두 요청을 동시에 시작
Task<PlayerData> playerTask = FetchPlayerDataAsync(playerId);
Task<LeaderboardData> boardTask = FetchLeaderboardAsync();
// 둘 다 완료될 때까지 대기
await Task.WhenAll(playerTask, boardTask);
return (playerTask.Result, boardTask.Result);
}
private async Task<LeaderboardData> FetchLeaderboardAsync()
=> await Task.FromResult(new LeaderboardData()); // 예시
}

적합한 경우: 게임 서버, API 클라이언트, 파일 I/O, 네트워크 통신

주의: async void는 예외를 삼키므로 이벤트 핸들러 외에는 사용하지 않습니다.


각 Actor는 독립적인 상태를 가지며, 메시지 큐를 통해서만 통신합니다. 공유 상태와 락이 없어 교착 상태가 발생하지 않습니다.

// C# — 단순 Actor 구현 (Channel 기반)
using System.Threading.Channels;
public class PlayerActor
{
private readonly Channel<IMessage> _mailbox =
Channel.CreateUnbounded<IMessage>();
private int _health = 100;
private int _score = 0;
public interface IMessage {}
public record TakeDamageMsg(int Amount) : IMessage;
public record AddScoreMsg(int Points) : IMessage;
public record GetStateMsg(
TaskCompletionSource<(int hp, int score)> Reply) : IMessage;
public void Send(IMessage msg) => _mailbox.Writer.TryWrite(msg);
public async Task RunAsync(CancellationToken ct)
{
await foreach (var msg in _mailbox.Reader.ReadAllAsync(ct))
{
switch (msg)
{
case TakeDamageMsg(var amt):
_health = Math.Max(0, _health - amt);
break;
case AddScoreMsg(var pts):
_score += pts;
break;
case GetStateMsg(var reply):
reply.SetResult((_health, _score));
break;
}
}
}
}
// 사용
var actor = new PlayerActor();
_ = actor.RunAsync(CancellationToken.None);
actor.Send(new PlayerActor.TakeDamageMsg(30));
actor.Send(new PlayerActor.AddScoreMsg(100));
var tcs = new TaskCompletionSource<(int, int)>();
actor.Send(new PlayerActor.GetStateMsg(tcs));
var (hp, score) = await tcs.Task;

적합한 경우: 게임 서버 플레이어 세션 관리, 분산 시스템, 공유 상태 없는 독립 유닛


항목ThreadCoroutineAsync/AwaitActor
스케줄링OS 선점형협력형I/O 이벤트 기반메시지 큐
메모리 비용높음 (MB)낮음 (KB)낮음낮음
공유 상태락 필요단일 스레드주의 필요없음
교착 상태 위험있음없음있음 (남용 시)없음
복잡도높음낮음중간중간

  • CPU 바운드 병렬 작업(물리, AI 배치)에는 스레드 풀이 적합하다.
  • 프레임 분산 처리나 타이밍 로직에는 코루틴이 가장 단순하다.
  • I/O 바운드 작업(네트워크, 파일)에는 async/await로 스레드를 절약한다.
  • 독립적 상태를 가진 유닛이 많은 게임 서버에는 Actor 모델이 교착 상태 없는 설계를 제공한다.