C++ Korea 2024 Hands-On(6월) 후기

인트로

더운 여름, 42 Seoul 본과정 막바지. 함께 공부하는 동료 T 님으로부터 소식을 하나 전해 들었다. “C++ Korea라는 단체에서 행사를 하는데 세션 중에 동시성 프로그래밍이 있다” 동료 T 님과 함께 멀티 쓰레드, 멀티 프로세스를 공부하고 과제를 함께 했던 경험이 있다. 우리 둘은 C++ 고수가 되고 싶은 열망이 가득하기 때문에 ‘한 번 가보자’로 의기투합하여 참석하기로 했다.

C++ Korea 2024 Meet-Up & Hands-On(6월)

C++ Korea는 C++ 한국 사용자 모임이고 옥찬호 개발자님께서 대표로 계신다. Meet-Up 세션은 C++ 개발자/사용자들의 고충과 일대기를 만담으로 진행되고 Hands-On 세션은 개발지식 Tutorial들을 가볍게 실습해보는 시간이다.

  • Hands-On: 빠르게 살펴 보는 C++ 동시성

개발에 있어 동시성 프로그래밍은 어려운 주제입니다. 공부하려고 해도 어려워서 진입을 못하는 경우도 있고, 공부하더라도 막상 적용하려니 어떻게 사용해야 할 지 막막한 경우도 있습니다. 이번 핸즈온에서는 스택을 예제로 싱글 스레드 버전부터 멀티 스레드, 락 프리까지 빠르게 살펴보고자 합니다. -소개글

D-day

위치는 SK 빌딩인 것 같은데 시설도 좋고 환경이 좋아서 동기부여가 되는 기분이다. 이번 Hands-On에는 10-15명 정도 모였다. 어떤 사람들이 모였는지 직접 대화를 해보지 않아 알 수는 없었지만, C++ 고수가 되고 싶은 열망으로 여길 찾아왔다는 점은 알 수 있었다.

사실 학교 밖, 실제 세상에 나온 첫 경험이다(개발 분야 한정). 학교 안에서는 내가 공부하는 내용이 맞는지 틀린지, 잘 공부하고 있는지 알기 어렵다. 혹시 내가 우물 안 개구리일까? 실제 개발자들의 세상은 어떤 느낌일까 궁금했었다. Hands-On 시간동안 질문을 여러번 했고, 나 이외에도 많은 사람들이 질문을 하고 함께 토론을 했다. 내가 좋아하는 생각을 남들도 똑같이 좋아하는 구나 싶은 생각이 들었다. 내가 앞으로 개발자들의 세상에 속한다면 즐겁고 행복할 것 같다.

그리고 유영천 개발자님의 모습도 보고 옥찬호 개발자님의 강의도 경험해보니, 개발 분야는 권위가 있고 경험이 많더라도 전혀 권위적이지 않은 것 같다. 나의 의문에 대해 이야기를 하고 질문을 해도 본질에 대해 고민하고, 권위자가 아닌 개발자로서 답변을 해주시는 모습이 인상적이었다.

새로운 인사이트

1. Pararellelism과 Concurrency는 다르다.

막연히 비슷한 개념이라고 생각하고 추상적으로만 알고 있었는데 강의 첫 도입부에 당연하다는 듯이 두개의 개념을 구분하여 설명하셨다. ‘어? 저걸 구분하는게 대단히 크리티컬하게 여기는건 아닌지만 구분되는건 당연하구나’ 라는 생각이 들었다. 기초는 쉬워서 기초가 아니라 가장 중요해서 기초이다. 그런 의미에서 동시성과 병렬성의 의미를 잘 이해하고 다뤄야할 것 같다.

2. 게임 클라이언트는 멀티스레드를 사용하지 않는다.

옥찬호 개발자님께서 강의 초반에 “멀티스레드가 싫으면 게임 클라이언트 쪽 가시면 됩니다” 라고 언급해주셨다. 나는 당연히 게임 개발 파트는 멀티스레드를 많이 다루게 될 것이라고 생각했었는데 그게 아니라고 해주셨다. 왜 그런가 생각을 해봐도 딱히 생각 나지 않아 강의가 끝나고 따로 질문을 드렸다. 옥찬호 개발자님의 답변을 다음과 같았다.

“만약 게임 클라이언트를 멀티스레드로 개발하게 된다면 락&언락 구역, 즉 크리티컬 섹션이 너무 길어진다. 잘 사용되진 않는다. 사용되더라도 엔진쪽, 쉐이더 정도에서 쓰인다.” 이 부분에 대해서 더 깊게 대화를 해보고 싶었지만 다음 일정때문에 길게 대화하지 못하고, 학교가는 길에 생각을 더 해보았다.

내 생각으로는 게임 클라이언트의 메인 루프 특성 때문인 것 같다. 멀티스레드 프로그래밍에는 동기화 문제(데드락, 레이스 컨디션)가 따른다. 디버깅과 유지보수가 어려워지는 것도 문제지만 컨텍스트 스위칭으로 인한 성능 오버헤드가 유발될 수 있다. 실시간으로 플레이 하는 게임에 있어서 컨텍스트 스위칭은 위험할 수 도 있을 것 같다. 특히 옥찬호 개발자님이 말씀하셨듯이 크리티컬 섹션이 길어질수록 스레드간의 경합이 증가하고, 이는 병목현상을 유발한다.

그럼 게임은 어떻게 동기화 문제를 해결할까? 라는 고민에서 생각난 것은 메인루프의 특성이다. 메인 루프가 한번 순회할 때, 모든 엔티티들의 Tick(deltaTime) 함수가 작동된다. 즉 이 메인 루프는 하나의 스레드에서 모든 렌더링, 업데이트, 이벤트 처리 등을 수행하기 때문에 자연스럽게 단일 스레드 환경이 되고, 자연스럽게 동기화 문제도 발생하지 않게 된다. 이는 소프트웨어 레벨(유저모드, 코드)에서 가상의 컨텍스트 스위칭을 구현한 것처럼 보일 수 있다. 게임 클라이언트의 메인 루프에서 각 엔티티의 상태를 업데이트하는 Tick(deltaTime) 함수는 일종의 가상 컨텍스트 스위칭처럼 작동한다. 이 방법을 통해 모든 엔티티를 빠르게 업데이트하고 렌더링할 수 있다. 이는 실질적인 멀티스레드의 컨텍스트 스위칭 없이도 유사한 효과를 낼 수 있다. 모든 작업이 16.6667ms(60fps 기준) 안에 완료되도록 설계되어야 하며, 이를 통해 실시간 게임 플레이의 동기화 문제를 해결할 수 있다.

이 부분에 대해서 더 많은 토론을 나눠보고 싶다.

3. atomic은 실제로 Lock-free가 아닐 수도 있다.

이 부분은 이번 Hands-On에서 옥찬호 개발자님의 강의의 핵심이었다.

C++ 표준에서는 std::atomic이 가능한 lock-free로 구현될 수 있어야 한다고 명시하고 있지만 실제로는 구현 세부 사항에 따라 lock-free가 작동되지 않을 수 있다.

예시 코드를 보여주셨는데, 다음과 같다.

struct doubleInt64
{
    std::int64_t v1, v2;
};

std::atomic<doubleInt64> adint64;

이 코드에서 adint64는 아토믹으로 선언했지만, Windows 플랫폼에서는 adint64.is_lock_free()에서 false가 나온다. atomic의 함정인 것이고, is_lock_free() 멤버 함수가 존재하는 것부터 아토믹이 아니라는 것을 암시한다.

이유는 atomic의 구현 방식때문이다. 나는 내부적으로 락을 걸어서 원자적 연산이 가능해지는 줄 알았는데, atomic의 원리는 단 하나의 CPU 명령어를 통해 연산하기 때문에 락을 걸지 않아도 원자적 연산이 가능해진다는 것이었다. 그렇기 때문에 CPU가 한번에 연산할 수 있는 크기를 넘는다면, 하나의 명령어로 연산이 불가능해지고, 이는 원자적 연산이 불가능해진다.

그럼 싱글 명령어로 해결이 안된다면, std::atomic은 내부적으로 락&언락을 걸어서 문제를 해결한다. 락&언락은 오버헤드가 심하기 때문에, atomic을 쓰고자 한다면 is_lock_free()를 통해 확인하는 것이 좋다.

남은 궁금증

실무에서 C++ thread 라이브러리를 사용하는가? 아니면 네이티브(Windows/POSIX) API를 쓰는게 유리한가? 레거시에서는 거의 안쓰일 것 같지만 신규 프로젝트에서는 모던 C++ thread 라이브러리 사용을 고려할까?

옥찬호 개발자님의 인사이트도 궁금하지만 내 생각에는 결국 네이티브 API를 래핑하는 이상 C++ thread를 이해할려면 결국 네이티브 API도 알아야될 것 같다. POSIX의 뮤텍스와 세마포어, 쓰레드, 포크만 알고 있는 상태인데, 윈도우 API도 공부한다면 플랫폼마다 어떤 점이 다를 수 있는지를 가늠할 수 있을 것이다.

닌텐도나 플레이스테이션같은 플랫폼에서는 하드웨어 API가 따로 존재할 것이다. 어떤 플랫폼이더라도 모두 핸들링이 가능한 개발자가 좋은 개발자가 아닐까? 라는 생각이 든다.

모든 플랫폼에서 핸들링이 가능한 개발자가 되는 것은 이상적인 목표일 수 있다. 현실적으로는 특정 플랫폼의 전문가가 되어 각 플랫폼의 전문가와 협업하는 것이 더 효율적일 수도 있겠다는 생각이 든다.

남은 궁금증2

게임 클라이언트는 멀티스레드를 정말 사용하지 않는건가?

옛날에는 싱글코어, 듀얼코어가 주류였는데, AMD에서 8코어 CPU를 출시한 적이 있다. 그 당시에는 멀티코어 CPU가 흔하지 않았지만 얼리어답터 성향때문에 덥석 구매했었다. 지금으로 치면 라이젠 7-9 급 CPU였기 때문에 성능이 좋긴 했었다. 다만 ‘시티즈 스카이라인’이라는 게임에서 문제가 있었다. 해당 게임은 싱글코어만 지원하기 때문에 나의 8코어 CPU중 1코어만 사용하게 되어버린 것이다. 덕분에 1/8 성능밖에 사용하지 못해 렉이 심하게 걸렸다.

요즘 게이밍 CPU들은 기본적으로 멀티코어를 지원하고 있는데 게임이 정말 싱글스레드 로직이라면 멀티코어보다 싱글코어 CPU가 더 유리한게 아닌가? 라는 생각이 든다. 멀티코어를 지원한다는 게임들은 어떤 태스크를 기준으로 스레드/프로세스를 나누는지 궁금해졌다.

엔딩

return 0;

Categories:

Updated:

Leave a comment