나만의 작은 도서관

[TIL][C++] 250429 MMO 서버 개발 7일차: Scatter-Gather, pch와 헤더, lock_guard와 mutex는 지역 변수이다. 그런데 어떻게 각 쓰레드가 락을 공유할까? 본문

Today I Learn

[TIL][C++] 250429 MMO 서버 개발 7일차: Scatter-Gather, pch와 헤더, lock_guard와 mutex는 지역 변수이다. 그런데 어떻게 각 쓰레드가 락을 공유할까?

pledge24 2025. 4. 30. 00:19
주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다. 

Scatter-Gather

  • scatter-gather란 데이터를 송신할때 송신할 데이터를 모아서 한 번에 보내는 방법을 말한다.

scatter-gather 방식을 사용하는 이유: 너무 잦은 시스템콜 호출 방지

  • 시스템 콜은 프로세스(또는 쓰레드)가 OS에게 서비스(파일 열기, 메모리 할당, 소켓 통신 등)을 요청하는 공식적인 인터페이스로, 시스템 콜 요청시 CPU는 해당 쓰레드를 사용자 모드 → 커널 모드로 전환한다. (즉, 시스템 콜은 다른 쓰레드에게 작업을 위임하는 것이 아니라, 동일한 쓰레드가 더 높은 권한을 일시적으로 얻어서 작업을 수행하는 것!)
  • 비동기 입출력 함수들은 시스템콜 방식으로 함수를 호출하며, 시스템콜 함수의 호출 비용은 일반 함수에 비해 높기 때문에 호출 횟수를 줄이는 것이 좋다.
  • 문제는 멀티 쓰레드 환경의 서버에서는 각 쓰레드에서 같은 소켓에 대한 송신 이벤트가 굉장히 많이 발생한다는 것이다. (해당 유저를 제외한 다른 유저의 이동 데이터가 오는 족족 해당 유저의 소켓에 송신하는 상황을 생각하면 된다.) 따라서, 송신 이벤트가 발생할때마다 WSASend()를 호출한다면 시스템콜을 매우 많이 하게 된다는 것이다.
  • 이러한 문제를 해결하기 위해 동일한 소켓에 한해서는 송신할 데이터를 모은 한 번에 WSASend()로 보내기로 한다. (Scatter-Gather방식)

scatter-gather 구상도 예제

class Session
{
public:
	/* 외부 사용 함수 */
	void send(SendBufferRef sendBuffer);
	
private:
	void gather(SendBufferRef sendBuffer);
	void processSend();
	
private:
	SOCKET _socket;
	queue<SendBufferRef> sendQueue; // 송신 데이터를 모을 자료 구조
}
  • send: 현재 세션의 소켓을 통해 데이터를 보내고자 한다. 내부적으로 gather를 실행한 다음, 충분히 모였으면 processSend()를 호출해 sendQueue에 모인 송신 데이터들을 WSASend()에 넣어 실행한다.
  • gather: sendQueue에 송신 데이터를 넣는다. 복사 비용을 아끼기 위해 값 대신 참조를 넣는다.
  • processSend: WSASend를 호출하는 함수. sendQueue에 모인 송신 데이터를 한 번에 보낸다.

pch와 헤더

  • 모든 헤더는 사용한 객체에 대해서 정의되어 있어야한다. 따라서, 현재 헤더 파일에 존재하지 않은 객체가 있다면 1) #include 전처리기 명령어를 통해 해당 객체의 클래스 정의를 가져오거나, 2) 전방 선언을 해야한다.
  • 헤더에 선언된 클래스를 정의한 .cpp 파일에 #include로 객체의 정의를 가져오면 되지 않을까? 싶지만 그렇지 않다. 각 파일은 독립적으로 존재했을때에도 모든 정의를 알 수 있어야한다. 따라서 아래는 **“컴파일 오류”**가 발생한다.
// a.h
class A
{
public:
	A();
private:
	std::vector<int> a; // vector를 모름. (컴파일 오류)
}

// a.cpp
#include <vector>
#include "a.h"

A::A(){}

pch(pre compiled header)의 사용시 달라지는 점.(MSVC 기준)

  • pch는 미리 컴파일된 헤더로, 컴파일 시 우선적으로 컴파일되며, 모든 소스 파일(.h, .cpp)에 대해 pch에 포함된 헤더에 정의된 객체들의 정의를 알 수 있다.
  • 따라서, 위 코드와 다르게 아래 코드는 컴파일 오류가 발생하지 않는다.
// pch.h (미리 컴파일된 헤더 -> 사용 설정됨)
#include <vector>
using namespace std;

// pch.cpp (미리 컴파일된 헤더 -> 만들기 설정됨)

// a.h
class A
{
public:
	A();
private:
	vector<int> a; // pch를 통해 vector를 알 수 있음.
}

// a.cpp
#include "a.h"

A::A(){}

lock_guard와 mutex는 지역 변수이다. 그런데 어떻게 각 쓰레드가 락을 공유할까?

  • lock_guardstd::mutex lg; 나 std::mutex m;에서 lg나 m은 각 쓰레드의 독립적인 스택에 저장된 지역 변수이다. 그럼에도 불구하고, m.lock()을 걸거나 lg를 선언하면 다른 쓰레드가 해당 코드에 락이 걸렸음을 인지할 수 있다.
  • 이는 lock_guard나 mutex가 내부적으로 락의 상태를 저장하는 동적 메모리를 가리키고 있기 때문.
  • 따라서, 코드 레벨에서 같은 줄 존재하는 객체가 건 락은 모든 쓰레드가 공유한다!