오브젝트풀은 소프트웨어 개발에서 성능 최적화를 위해 사용되는 디자인 패턴이다.
객체 생성과 소멸에 드는 비용을 줄이기 위해 미리 생성된 객체들을 풀(Pool)에 보관하고 재사용하는 방식이다.

📌 핵심 개념

ObjectPool의 기본 아이디어는 비용이 많이 드는 객체들을 필요할 때마다 새로 생성하지 않고, 미리 생성해두고 재사용하는 것이다. 객체를 사용한 후에는 다시 풀로 반환하여 다른 곳에서 재사용할 수 있게 한다.

 

📌 동작 원리

  1. 초기화: 풀 생성 시 일정 수의 객체를 미리 생성하여 풀에 저장
  2. 대여(Rent): 객체를 요청하면 풀에서 사용 가능한 객체를 제공
  3. 반환(Return): 사용이 끝난 객체를 다시 풀로 반환
  4. 재사용: 반환된 객체는 초기화된 후 다른 요청에서 재사용

 

앞서 작업하던 채팅서버에서 Packet을 오브젝트풀을 사용해 할당하려고한다.

Packet은 멀티스레드에서 사용해야하기 때문에 앞서 포스팅한 Lock-free Stack을 이용해 오브젝트 풀을 구현했다.

 

Alloc 함수

template<class DATA>
DATA* ObjectPool<DATA>::Alloc(void)
{
	Node* AllocNode;
	CheckNode Top;
	int AllocCount = _AllocCount;
	int UseCount = _UseCount;

	InterlockedIncrement(&_UseCount);

	if (AllocCount > _UseCount) //사용 할 수 있는 블럭이 있는 경우
	{
		LONG64 LockFreeCheckCount = InterlockedIncrement64(&_LockFreeCheckCount);

		do
		{
			Top.NodeCheckValue = _TopNode->NodeCheckValue;
			Top.TopNode = _TopNode->TopNode;

			AllocNode = Top.TopNode;
		} while (!InterlockedCompareExchange128((volatile LONG64*)_TopNode, LockFreeCheckCount, (LONG64)_TopNode->TopNode->NextBlock, (LONG64*)&Top));

		new (AllocNode) DATA;
	}
	else //사용 할 수 있는 블럭이 없을 경우
	{
		//새로운 노드 할당해주고 생성자 호출 후 넘겨줌

		AllocNode = (Node*)malloc(sizeof(Node));
		memset(AllocNode, 0, sizeof(Node));
		new (AllocNode) DATA;

		/*
			할당 수와 사용 수를 늘려준다.
		*/
		InterlockedIncrement(&_AllocCount);
	}

	return (DATA*)&AllocNode->Data;
}

 

AllocCount와 UseCount를 비교해서 새로 할당해주거나 Stack으로 관리하는 데이터를 하나꺼내서 반환한다.

 

Free 함수

template<class DATA>
bool ObjectPool<DATA>::Free(DATA* Data)
{
	/*
		받은 데이터를 풀에 넣기 ( Push )
	*/
	CheckNode Top;
	Node* FreeNode = (Node*)Data;

	do
	{
		Top.NodeCheckValue = _TopNode->NodeCheckValue;
		Top.TopNode = _TopNode->TopNode;

		FreeNode->NextBlock = Top.TopNode;
	} while (!InterlockedCompareExchange128((volatile LONG64*)_TopNode, Top.NodeCheckValue, (LONG64)FreeNode, (LONG64*)&Top));

	InterlockedDecrement(&_UseCount);

	return true;
}

 

사용완료한 데이터를 풀에 넣는다.

'언어 > C' 카테고리의 다른 글

[C] 메모리풀(Memory Pool )  (0) 2025.06.14
[C] Lock-free Stack  (0) 2025.06.14
[C] Lock-free 알고리즘  (0) 2025.06.13
[C] TLS ( Thread Local Storage ) - 스레드 지역 저장소  (0) 2025.06.13
[C] HANDLE 닫고 NULL 넣는 이유  (0) 2025.06.02
메모리풀은 메모리 할당과 해제를 효율적으로 관리하기 위한 메모리 관리 기법이다.
시스템의 일반적인 동적 메모리 할당 대신 미리 할당된 메모리 블록들을 재사용하여 성능을 향상시키는 방식이다.

 

📌 핵심 개념

메모리풀은 프로그램 시작 시 큰 메모리 블록을 미리 할당하고, 이를 작은 단위로 나누어 관리한다. 

프로그램이 메모리를 요청하면 풀에서 사용 가능한 블록을 할당하고, 사용이 끝나면 다시 풀로 반환한다.

 

📌 일반 메모리 할당 vs 메모리풀

일반 메모리 할당(malloc/new)

  • 운영체제에 메모리 요청
  • 메모리 단편화 발생 가능
  • 할당/해제 시스템 콜 오버헤드
  • 예측 불가능한 할당 시간

메모리풀

  • 미리 할당된 메모리에서 빠른 할당
  • 단편화 최소화
  • 예측 가능한 성능
  • 시스템 콜 오버헤드 감소

 

📌 메모리풀 유형

  1. 고정 크기 블록 풀: 모든 블록이 동일한 크기로 관리되는 방식
  2. 가변 크기 블록 풀: 다양한 크기의 블록을 지원하는 방식
  3. 스택 기반 풀: LIFO 방식으로 메모리를 할당/해제하는 방식
  4. 링 버퍼 풀: 순환 버퍼 형태로 메모리를 관리하는 방식

'언어 > C' 카테고리의 다른 글

[C] 오브젝트풀(Object Pool)  (0) 2025.06.14
[C] Lock-free Stack  (0) 2025.06.14
[C] Lock-free 알고리즘  (0) 2025.06.13
[C] TLS ( Thread Local Storage ) - 스레드 지역 저장소  (0) 2025.06.13
[C] HANDLE 닫고 NULL 넣는 이유  (0) 2025.06.02
Lock-free Stack은 동기화 객체 없이 동시성을 보장하는 스택 자료구조를 말한다.
주로 Compare-And-Swap( CAS ) 같은 원자적 연산을 활용하여 구현된다.

 

📌 핵심 개념

Lock-free의 정의: 적어도 하나의 스레드가 유한한 시간 내에 진행을 보장받는 알고리즘이다.

하나의 스레드가 무한히 지연되더라도 다른 스레드들은 계속 작업을 수행할 수 있다.

 

📌 구현 원리

Lock-free Stack은 단방향 연결 리스트로 구현되고, head 포인터에 대한 원자적 업데이트를 통해 동작한다.

Push 연산

  1. 새 노드를 생성하고 데이터를 저장
  2. 새 노드의 next를 현재 head로 설정
  3. CAS를 사용하여 head를 새 노드로 원자적으로 업데이트
  4. CAS가 실패하면 다른 스레드가 head를 변경한 것이므로 재시도

Pop 연산

  1. 현재 head 읽기
  2. head가 null이면 스택이 비어있음
  3. head->next를 새로운 head로 설정하려고 CAS 시도
  4. CAS가 성공하면 이전 head 노드의 데이터 반환
  5. 실패하면 재시도

 

 

'언어 > C' 카테고리의 다른 글

[C] 오브젝트풀(Object Pool)  (0) 2025.06.14
[C] 메모리풀(Memory Pool )  (0) 2025.06.14
[C] Lock-free 알고리즘  (0) 2025.06.13
[C] TLS ( Thread Local Storage ) - 스레드 지역 저장소  (0) 2025.06.13
[C] HANDLE 닫고 NULL 넣는 이유  (0) 2025.06.02
Lock-free 알고리즘은 멀티스레드 환경에서 동기화 메커니즘을 사용하지 않고 스레드 간 동기화를 달성하는 프로그램 기법을 말한다.

 

📌 핵심 개념

Lock-free 알고리즘은 CAS( Compare-And-Swap )이나 Load-Link/Store-Conditional 같은 원자적 연산을 사용한다.

이 연산들은 하드웨어 레벨에서 원자성을 보장하므로 락 없이도 데이터 일관성을 유지할 수 있다.

 

CAS 연산은 어떤 데이터를 비교하고, 해당 데이터가 같으면 값을 교환하고 true 또는 false를 반환하는 작업에 원자성을 보장한다.

if(memory_location == expected_value)
{
    memory_location = new_value;
    return true;
}
else
{
    return false;
}

 

📌 ABA Problem

CAS를 사용해 자료구조의 아이템을 변경할 때, 포인터가 시스템에 의해 재사용되면서 생기는 문제를 말한다.

예를 들어보자.

ThreadA, ThreadB, ThreadC가 있고,

{ 4, 3, 2, 1 } 순서로 네 개의 수를 삽입한 스택이 있다고 할때, ThreadA에서 pop을 해서 4를 빼고, 3을 top으로 지정하는 작업을 완료하기 전에, ThreadB에서 접근해서 pop을 2번 진행한후, ( { 2, 1 } ) ThreadC에서 push를 해서 4를 다시 넣은 후 ( { 4,2,1} ), ThreadA에서 CAS를 하면 ThreadA에서는 4가 맨위에 있기 때문에 아무런 문제가 발생하지 않은 것처럼 보인다. 따라서 3을 top으로 만들게 되는데, 3은 이미 제거된 데이터이기 때문에 3을 top으로 보면 안된다.

이와 같은 문제가 대표적인 ABA Problem이라고 할 수 있다.

'언어 > C' 카테고리의 다른 글

[C] 메모리풀(Memory Pool )  (0) 2025.06.14
[C] Lock-free Stack  (0) 2025.06.14
[C] TLS ( Thread Local Storage ) - 스레드 지역 저장소  (0) 2025.06.13
[C] HANDLE 닫고 NULL 넣는 이유  (0) 2025.06.02
[C] 미리 컴파일된 헤더  (0) 2025.05.29
TLS는 각 스레드마다 독립적으로 가지는 저장공간이다.
같은 변수나 메모리 영역이라도 스레드마다 다른 값을 가질 수 있게 해주는 메커니즘

 

📌 기본 개념

  • 전역 변수의 문제점 : 모든 스레드가 공유하므로 동기화 필요.
  • TLS의 해결책 : 각 스레드마다 독립적인 저장공간을 제공
  • 키 - 값 구조 : TLS 인덱스(키)를 통해 각 스레드별 데이터에 접근

 

📌 기본함수들

 

// TLS 인덱스 할당
DWORD TlsAlloc(void);

// TLS에 값 저장 (스레드별로 독립적)
BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue);

// TLS에서 값 가져오기 (스레드별로 독립적)
LPVOID TlsGetValue(DWORD dwTlsIndex);

// TLS 인덱스 해제
BOOL TlsFree(DWORD dwTlsIndex);

 

📌 동작 원리

DWORD g_TlsIndex;

void ThreadFunction()
{
    // 각 스레드마다 다른 값을 저장할 수 있음
    int* myData = new int(GetCurrentThreadId());
    TlsSetValue(g_TlsIndex, myData);
    
    // 나중에 이 스레드에서 다시 호출하면 같은 값이 나옴
    int* retrievedData = (int*)TlsGetValue(g_TlsIndex);    
}

 

📌 TLS( Thread Local Storage ) 메모리 구조

Process Memory Space
├── Global/Static Memory (모든 스레드 공유)
├── Heap Memory (모든 스레드 공유)
└── Thread Specific Areas
    ├── Thread 1
    │   ├── Stack
    │   └── TLS Area (Index 0: ptr1, Index 1: ptr2, ...)
    ├── Thread 2
    │   ├── Stack  
    │   └── TLS Area (Index 0: ptr3, Index 1: ptr4, ...)
    └── Thread 3
        ├── Stack
        └── TLS Area (Index 0: ptr5, Index 1: ptr6, ...)

 

📌 실제 동작 예제

class Example 
{
private:
    DWORD _tlsIndex;          // 모든 스레드가 공유하는 인덱스
    static int s_globalCount; // 모든 스레드가 공유하는 변수
    
public:
    Example() {
        _tlsIndex = TlsAlloc();
    }
    
    void SetThreadData(int value) {
        // 각 스레드마다 다른 메모리 위치에 저장됨
        int* data = new int(value);
        TlsSetValue(_tlsIndex, data);
    }
    
    int GetThreadData() {
        // 현재 스레드의 데이터를 반환
        int* data = (int*)TlsGetValue(_tlsIndex);
        return data ? *data : 0;
    }
};

 

'언어 > C' 카테고리의 다른 글

[C] Lock-free Stack  (0) 2025.06.14
[C] Lock-free 알고리즘  (0) 2025.06.13
[C] HANDLE 닫고 NULL 넣는 이유  (0) 2025.06.02
[C] 미리 컴파일된 헤더  (0) 2025.05.29
[C] 템플릿 함수를 헤더에 정의해야하는 이유  (0) 2025.05.29

보통 HANDLE을 CloseHandle을 이용해서 닫고 해당 HANDLE에 NULL을 넣는다. 

배울때 습관적으로 넣었는데, 왜 넣어야하는지 그 이유를 알고 싶어졌다.

 

✅ 이중 해제 방지

핸들을 NULL로 만들어 두면 이후 코드에서 if문으로 유효한지 확인하고 다시 닫는 실수를 방지할 수 있다.

 

✅ 사용 후 참조 방지

닫힌 핸들을 NULL로 바꿔두면 잘못된 참조나 오용을 방지할 수 있다.

그렇지 않으면 나중에 그 핸들을 유효한 것처럼 잘못 사용하여 예외나 오류가 발생할 수 있다.

 

✅ 디버깅 시 추적이 쉬움

디버깅 중 핸들이 NULL이면 해당 핸들이 '해제된 상태'라는 것을 쉽게 파악할 수 있다.

 

c++11 이상이라면 nullptr이 더 의미상 명확하고 권장된다.

하지만 HANDLE은 전통적인 C 스타일 핸들이므로, NULL도 문제 없이 동작한다.

다만 resource를 닫고 나서 '이 핸들은 무효하다'는 의도를 표현할 때는 nullptr이 의미적으로는 더 정확하다.

미리 컴파일된 헤더( Precompiled Header, PCH )는 컴파일 시간을 단축하기 위해 자주 변경되지 않는 헤더 파일을 미리 컴파일해두고, 이후 빌드 시 다시 컴파일하지 않고 재사용하는 기능을 말한다.

 

✅ 미리 컴파일된 헤더의 개념

  • c++ 프로젝트에서 <windows.h>, <iostream> 같은 무거운 헤더 파일들은 컴파일러가 파싱하는 데 상대적으로 시간이 오래 걸린다.
  • 이러한 헤더들을 한 번만 컴파일하고 결과를 캐시처럼 재사용하면 전체 컴파일 속도가 빨라진다.
  • 이를 위해 컴파일러는 .pch라는 파일로 헤더의 중간 결과물을 저장해 둔다.

 

✅ 사용 방법

// pch.h
#pragma once
#include <iostream>
#include <vector>
#include <Windows.h>

pch용 헤더 파일 생성 ( pch.h )

 

// pch.cpp
#include "pch.h"

pch를 실제로 컴파일할 cpp 파일 생성 ( pch.cpp )

 

 

프로젝트 속성 - C/C++ - 미리 컴파일된 헤더

미리 컴파일된 헤더를 사용(/Yu)

미리 컴파일된 헤더 파일을 pch.h로 설정하고 확인

 

이후 생성된 cpp파일들은 자동으로 pch.h를 포함한다.

✅ 장점

  • 빌드 속도 향상
  • 자주 바뀌지 않는 공통 헤더는 한 번만 컴파일

 

⚠️ 주의할 점

  • pch.h가 변경되면 다시 컴파일되어야 함 ( 자주 바뀌는 헤더는 pch에 포함하지 않는 것이 좋다. )
  • 컴파일 에러가 나면 디버깅 힘듬 ( 헤더의 순서와 포함 관계에 주의 )

✅ c++ 템플릿의 컴파일 원리

템플릿은 컴파일 타임에 인스턴스화된다.

 

c++ 에서 템플릿은 실제로 사용되기 전까지 코드가 생성되지 않는다. 템플릿 함수나 클래스는 일종의 코드 생성기로,

특정 타입이 주어졌을 때 컴파일러가 해당 타입에 맞는 코드를 만들어낸다.

예를 들어

template<typename T>
T add(T a, T b)
{
      return a + b;
}

 

add<int>(1, 2)가 호출될 때, int를 타입으로 가지는 add<int>라는 함수가 생성된다.

 

✅ 헤더에 정의하지 않으면 생기는 문제

만약 위 템플릿을 .cpp 파일에 정의하고, 헤더에는 선언만 해두면 아래와 같은 문제가 발생한다.

 ❗ 컴파일러가 템플릿 정의를 찾을 수 없음

  • 컴파일러는 어떤 타입으로 인스턴스화할지 알 수 없다.
  • 컴파일러가 .cpp 파일을 보지 않고 템플릿의 정의 전체를 알아야 해당 타입에 맞는 코드를 생성할 수 있기 때문

결과적으로 링커 에러가 발생한다.

undefined reference to 'int add<int>(int, int)'

 

✅ 해결 방법: 정의를 헤더에 포함

템플릿은 정의와 선언을 한 파일( 주로 헤더 )에 함께 작성해야 아래와 같이 동작한다.

  • 헤더는 cpp 파일에서 #include 되기 때문에 정의가 항상 접근이 가능
  • 컴파일러는 그 자리에서 템플릿 인스턴스를 직접 생성할 수 있다.
// add.h
template<typename T>
T add(T a, T b) {
    return a + b;
}

 

헤더에 선언과 정의를 동시에 구현

 

#include"add.h"
int result = add<int>(3, 4);

 

위처럼 add함수를 호출하는 순간 컴파일러가 add<int>를 생성한다.

 

 

'언어 > C' 카테고리의 다른 글

[C] HANDLE 닫고 NULL 넣는 이유  (0) 2025.06.02
[C] 미리 컴파일된 헤더  (0) 2025.05.29
[C] 2진수, 16진수 변환  (0) 2025.04.23
[C] 컴퓨터 덧셈 ( 반가산기, 전가산기 )  (0) 2025.04.23
[C] 게이트 회로  (0) 2025.04.23

2진수 

  • 기수(base)가 2인 수 체계를 말한다.
  • 사용 가능한 수는 0과 1 이다.

16진수

  • 기수(base)가 16인 수 체계를 말한다.
  • 사용 가능한 수는 0 ~ 9, A ~ F ( A = 10, F = 15 )

 

2진수 -> 16진수 변환 방법

  1. 2진수를 오른쪽부터 4자리씩 끊어서 그룹으로 묶는다.
  2. 각 4자리 그룹을 10진수로 바꾼 뒤, 해당하는 16진수로 변환한다.

예제 

2진수: 10110111

1. 4자리씩 나누기:      1011   0111
2. 각각 10진수로:        11     7
3. 16진수로 변환:        B      7

=> 결과: 0xB7

 

16진수 -> 2진수 변환 방법

각 16진수 자릿수를 개별적으로 2진수 4자리로 변환한다.

예제

16진수: 9A

1. '9' → 1001
2. 'A' (10) → 1010

결과: 0b10011010

 

16진수 <-> 2진수 매핑 테이블

16진수 2진수
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001
A 1010
B 1011
C 1100
D 1101
E 1110
F 1111

 

반가산기

반가산기

반 가산기는 2개의 2진수  A,B 논리변수를 더하여 합(SUM)과 캐리(Carry)를 산출하기 위한 조합 논리회로다.

  • 입력 받을 수 있는 수는 A, B 2가지 각각 0 또는 1을 입력한다.
  • 2진수 1 + 1은 2진수 10(2)다.
  • A가 1, B가 1이면 XOR 연살결과 S는 0이다.
  • 동시에 A가 1, B가 1이면 AND 연산결과 C는 1이다. 이 1은 자리올림을 나타낸다.

🔷 반가산기 진리표

A B S C
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1

 

보통 4비트씩 묶어서 계산하기 때문에 반가산기만으로는 덧셈을 온전히 처리할 수 없다.

3자리 이상부터 자리 올림이 발생하면, 총 3가지의 수가 입력되어야하는데 반가산기는 2개의 입력만 받을 수 있기 때문이다.


전가산기

전가산기

컴퓨터 내부에서 여러 비트로 된 두 수를 더할 때 두 비트에서 더해진 결과인 캐리(Carry)는 더 높은 자리의 두 비트의 덧셈에 추가되어 더해진다. 이때, 아래 자릿수에서 발생한 캐리까지 포함하여 세 비트를 더하는 논리회로를 전가산기라고 한다.

전가산기는 3개의 입력을 받을 수 있다.

 

🔷 전가산기 진리표

A B Cin S Cout
0 0 0 0 0
0 0 1 1 0
0 1 0 1 0
0 1 1 0 1
1 0 0 1 0
1 0 1 0 1
1 1 0 0 1
1 1 1 1 1

 

'언어 > C' 카테고리의 다른 글

[C] HANDLE 닫고 NULL 넣는 이유  (0) 2025.06.02
[C] 미리 컴파일된 헤더  (0) 2025.05.29
[C] 템플릿 함수를 헤더에 정의해야하는 이유  (0) 2025.05.29
[C] 2진수, 16진수 변환  (0) 2025.04.23
[C] 게이트 회로  (0) 2025.04.23

+ Recent posts