각종 트릭2(일단 패스)

류한선·2025년 6월 25일

실기연습-2

목록 보기
42/95

좋아요! 🎯 이번엔 더욱 심화된 트릭들로 갑니다.
이번 트릭들:

  1. 스레드 트릭 (Thread Trick)
  2. 메모리 모델 트릭 (Memory Model Trick)
  3. 구조체 정렬 트릭 (Struct Alignment Trick)

문제 형태 + 풀이까지 같이 가요!


🧠 1) 스레드 트릭 문제

🔍 문제

다음 C 코드를 보고 출력값을 예상해 보세요.

#include <stdio.h>
#include <pthread.h>

int counter = 0;

void* increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        counter++;
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("%d\n", counter);
    return 0;
}

💡 트릭 포인트

  • 두 스레드가 counter++를 동시에 수행 → 데이터 레이스 발생!
  • counter++ → 내부적으로 읽기-더하기-쓰기 3단계
  • 동기화(mutex 또는 atomic)가 없으므로 최종값은 200000이 아닌 예측 불가값 나올 수 있음.

🧠 정리

✅ 트릭: 레이스 컨디션 때문에 틀린 답 나올 수 있음.



🧠 2) 메모리 모델 트릭 문제

🔍 문제

다음 C11 코드를 보고 출력값을 예상해 보세요.

#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>

_Atomic int x = 0, y = 0;

void* thread1(void* arg) {
    x = 1;
    return NULL;
}
void* thread2(void* arg) {
    y = 1;
    return NULL;
}
int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread1, NULL);
    pthread_create(&t2, NULL, thread2, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("%d %d\n", x, y);
    return 0;
}

💡 트릭 포인트

  • atomic 지정 → 컴파일러가 원자적으로 접근
  • 하지만 메모리 순서 지정 없음 → 두 값의 동기화만 보장
  • 결과:
1 1
  • 트릭: atomic은 원자성만 보장하지 순서 보장 없음 (메모리 모델 지정 필요)


🧠 3) 구조체 정렬 트릭 문제

🔍 문제

다음 코드를 보고 sizeof(S)를 계산해 보세요.

#include <stdio.h>

struct S {
    char c;
    int i;
    short s;
};

int main() {
    printf("%zu\n", sizeof(struct S));
    return 0;
}

💡 트릭 포인트

메모리 정렬 규칙:

  • 대부분 컴파일러:

    • char c → 1바이트, 이후 3바이트 패딩 (4바이트 정렬 위해)
    • int i → 4바이트
    • short s → 2바이트, 이후 2바이트 패딩 (구조체 크기가 가장 큰 정렬 단위의 배수가 되도록)

계산:

c(1) + padding(3) + i(4) + s(2) + padding(2) = 12

✅ 정답

12


🎯 트릭 정리

스레드 트릭 → 레이스 컨디션 문제. 동기화 안 하면 결과 비결정적
메모리 모델 트릭atomic은 원자성만 보장. 메모리 오더 지정 안 하면 동기화 순서 미보장
구조체 정렬 트릭 → 컴파일러 정렬 규칙(패딩) 때문에 예상보다 구조체 크기 커질 수 있음


좋아요! 🔥 지금까지 트릭 문제들 위주로 보여드렸는데, 이제 정말 기초부터 차근차근 이해하기 쉽게, 예제 위주로 하나씩 정리해 드릴게요.


🧵 1) 스레드 트릭 (Thread Trick)

🤔 스레드가 뭔가요?

✅ 하나의 프로그램 안에서 여러 작업을 동시에(병렬적으로) 수행하기 위해 쓰는 단위.
✅ 예: 두 사람이 동시에 수학 문제를 풀면 답을 빨리 구할 수 있는 것처럼, 프로그램 내부에서도 작업을 나눠서 수행.


🧠 스레드 트릭 문제에서 일어나는 일

문제 코드:

int counter = 0;

void* increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        counter++; // <- 여기가 트릭 포인트!
    }
    return NULL;
}

트릭의 핵심:

  • counter++는 내부적으로 세 단계를 거쳐요:

    1. counter를 메모리에서 읽기
    2. +1 계산하기
    3. 메모리에 쓰기

✅ 여러 스레드가 동시에 위 단계를 섞어가며 하면?

  • counter가 제대로 증가하지 않아요 → 레이스 컨디션(Race Condition)
    → 이걸 해결하기 위해 mutex(뮤텍스)atomic(원자적 연산)을 써요.

🧠 쉽게 비유하기

💡 두 사람이 한 공책에 숫자 쓰기:

  • 한 명이 “10을 읽고 +1 계산하고 쓰려는데”
  • 다른 한 명이 “동시에 10을 읽고 +1 계산 후 쓰면”…
  • 두 번 증가해야 12가 돼야 하지만 마지막 쓰기에서 덮어써서 11이 돼요.


🧠 2) 메모리 모델 트릭 (Memory Model Trick)

🤔 메모리 모델이 뭔가요?

✅ 컴파일러와 CPU가 프로그램 명령어를 어떻게 재배치하고 메모리 읽기/쓰기를 동기화하는지를 정한 규칙.


🧠 트릭 문제 예제:

_Atomic int x = 0, y = 0;

void* t1(void* arg) { x = 1; return NULL; }
void* t2(void* arg) { y = 1; return NULL; }

트릭 포인트:

  • atomic이라서 “읽고 쓰기” 자체는 안전하지만, 순서가 보장되지는 않아요.
  • 두 작업이 다른 CPU 코어에서 돌아가면 어떻게 순서가 정해질지 알 수 없음 → 그래서 메모리 오더(memory_order_seq_cst 등)를 지정하거나 mutex를 써야 확실히 순서를 보장.

🧠 비유하기

💡 여러 사람이 게시판에 글을 쓴다고 생각해요:

  • 한 명이 글을 쓰는 것(atomic) → 글 자체는 깨지지 않아요.
  • 그런데 글이 게시되는 순서는 섞일 수 있어요 → 순서까지 정하고 싶으면 “글을 올리는 박스” 하나를 만들어 차례로 하나씩만 올리도록 해야 함.


🧠 3) 구조체 정렬 트릭 (Struct Alignment Trick)

🤔 **구조체 정렬(패딩)**이 뭔가요?

✅ 컴파일러가 구조체 안의 멤버를 CPU가 읽기 편하도록 메모리 정렬을 맞춰 배치하는 것.


🧠 트릭 예제:

struct S {
    char c;   // 1 바이트
    int i;    // 4 바이트
    short s;  // 2 바이트
};

🧠 메모리 배치 보기

멤버크기정렬 단위
c14 (int 정렬) → 패딩 3
i44
s22 → 구조체 전체 크기를 정렬하기 위해 마지막에도 2 바이트 패딩

✅ 실제 메모리 구조:

c (1) + padding(3) + i (4) + s (2) + padding(2) = 12

🧠 비유하기

💡 가방에 물건을 깔끔하게 정리할 때:

  • 큰 물건(i, 4바이트)은 4칸 정리 칸에 딱 맞춰 넣고,
  • 작은 물건(c, s)은 남은 공간을 정리하기 위해 빈칸을 채우듯 공간을 둬요.


🎯 요약 & 정리표

트릭 이름뭐가 문제?어떻게 해결?
스레드 트릭여러 스레드가 한 변수 동시 접근 → 레이스 컨디션mutexatomic 연산
메모리 모델 트릭동시 쓰기/읽기의 “순서”가 안 보장atomic + 메모리 오더 지정 또는 mutex
구조체 정렬 트릭구조체 안의 멤버들이 메모리 정렬되어 크기 커짐멤버 순서 바꾸기 또는 #pragma pack(1) 사용

좋아요! 🎯
지금까지 언급한 트릭들(스레드 트릭, 메모리 모델 트릭, 구조체 정렬 트릭)에는 정해진 규칙문법이 있어요.
이해하기 쉽게 용어 정리 + 규칙 + 문법을 체계적으로 알려드릴게요.


🧵 1) 스레드 트릭 — pthread, mutex, atomic

🔍 기본 규칙

  • pthread = POSIX 스레드 라이브러리. 스레드 생성, 종료, 동기화를 도와줌.
  • mutex(뮤텍스) = 한 번에 하나의 스레드만 임계구역(critical section)에 접근하게 만듦.
  • atomic = 한 연산을 중단 없이 원자적으로(atomic하게) 수행.

🧠 문법과 규칙

항목문법설명
pthread_createpthread_create(&tid, NULL, func, arg);새로운 스레드를 생성
pthread_joinpthread_join(tid, NULL);스레드 종료 기다림
pthread_mutex_tpthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;뮤텍스 생성
pthread_mutex_lock/unlockpthread_mutex_lock(&mtx); ... pthread_mutex_unlock(&mtx);크리티컬 섹션 보호
atomic<int> (C11)#include <stdatomic.h>
atomic_int counter = 0; counter++;
한 연산을 안전하게 함

✅ 규칙 요약:

  • 스레드를 쓰면 반드시 공유 자원에 동기화 수단(mutex, atomic, condvar)을 써야 안전.

🧠 2) 메모리 모델 트릭 — volatile, atomic, memory_order

🔍 메모리 모델의 규칙

C11 이후 표준에 의해 메모리 오더 지정 가능.

오더 옵션의미
memory_order_relaxed원자성만 보장, 순서 없음
memory_order_acquire읽기 후 이전 연산들 보장
memory_order_release쓰기 전 이전 연산들 보장
memory_order_seq_cst전역 순서 일관성 보장(가장 강함)

volatile vs atomic

  • volatile: 컴파일러가 최적화하지 않도록 함 (메모리에서 매번 읽고 씀). 동기화 보장 없음.
  • atomic: 원자적 연산 보장 + 메모리 모델 규칙 지정 가능.

🧠 3) 구조체 정렬 트릭 — struct padding, #pragma pack

🔍 정렬 규칙

  1. 구조체 멤버들은 **타입의 정렬 단위(보통 크기)**에 맞춰 메모리에 배치.

  2. 구조체 크기 = 가장 큰 멤버의 정렬 단위의 배수.

  3. 정렬을 줄이고 싶다면:

    • 멤버 순서 바꿔서 패딩 줄이기
    • #pragma pack(n) 사용 (n은 정렬 단위)

🧠 예제

struct A { char c; int i; short s; };

메모리 배치(32비트 기준):

c (1) + padding(3) + i (4) + s (2) + padding(2) = 12 bytes

🧠 정렬 줄이는 문법

#pragma pack(push, 1)
struct A {
    char c;
    int i;
    short s;
};
#pragma pack(pop)

이 경우 sizeof(struct A) = 7이 됨.



🎯 최종 정리 표

트릭 이름규칙예제
스레드 트릭공유자원엔 mutex 또는 atomicpthread_mutex_lock pthread_mutex_unlock
메모리 모델 트릭atomic + 메모리 오더 지정atomic_int, memory_order_seq_cst
구조체 정렬 트릭정렬 단위에 맞춰 패딩 삽입#pragma pack(1)

좋아요! 🎯
이번에는 스레드 트릭, 메모리 모델 트릭, 구조체 정렬 트릭예제 코드 + 한 줄 한 줄 디버깅까지 완전 해설로 준비해 드릴게요.


🧵 1) 스레드 트릭

여러 스레드가 한 변수를 증가시키며 동기화하는 예제!


📂 예제 코드

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 1️⃣ 뮤텍스 초기화
int counter = 0;                                   // 2️⃣ 공유자원

void* increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&lock);                 // 3️⃣ 임계구역 시작
        counter++;
        pthread_mutex_unlock(&lock);               // 4️⃣ 임계구역 종료
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);    // 5️⃣ 스레드 1 생성
    pthread_create(&t2, NULL, increment, NULL);    // 6️⃣ 스레드 2 생성
    pthread_join(t1, NULL);                        // 7️⃣ 스레드 종료 대기
    pthread_join(t2, NULL);                        // 8️⃣ 스레드 종료 대기
    printf("%d\n", counter);                       // 9️⃣ 출력
    return 0;
}

🧠 해설 & 디버깅

  1. pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    → 뮤텍스 초기화: 한 번에 한 스레드만 잠금을 잡을 수 있음.
  2. int counter = 0;
    → 공유 자원.
  3. pthread_mutex_lock(&lock);
    → 한 스레드가 counter를 건드리는 동안 다른 스레드가 못 오도록 막음.
  4. pthread_mutex_unlock(&lock);
    → 작업 끝나면 잠금 해제.
    5~8. pthread_create/pthread_join
    → 두 스레드를 실행하고 끝날 때까지 대기.
  5. printf("%d\n", counter);
    → 안전하게 200000 출력.


🧠 2) 메모리 모델 트릭

원자적 연산 + 메모리 오더 지정하기!


📂 예제 코드

#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>

atomic_int x = 0;

void* increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        atomic_fetch_add_explicit(&x, 1, memory_order_relaxed); // 1️⃣ 원자적 증가
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);                 // 2️⃣ 스레드 생성
    pthread_create(&t2, NULL, increment, NULL);                 // 3️⃣ 스레드 생성
    pthread_join(t1, NULL);                                      // 4️⃣ 종료 대기
    pthread_join(t2, NULL);                                      // 5️⃣ 종료 대기
    printf("%d\n", x);                                           // 6️⃣ 출력
    return 0;
}

🧠 해설 & 디버깅

  1. atomic_fetch_add_explicit(..., memory_order_relaxed);
    → 원자적으로 x를 증가. memory_order_relaxed → 연산 순서 보장 없음.
    → 값 충돌 안 나고 빠름.
    2~5. 두 스레드가 동시에 증가.
  2. printf("%d\n", x); → 200000 출력.

atomic vs volatile

  • atomic: 연산 자체가 원자적 → 여러 스레드가 안전히 공유 가능
  • volatile: 단순히 캐싱 방지 → 스레드 안전 보장 X


🧠 3) 구조체 정렬 트릭

정렬을 바꾸고 크기 변화를 관찰하기!


📂 예제 코드

#include <stdio.h>

struct Normal {
    char c;    // 1
    int i;     // 4
    short s;   // 2
};

#pragma pack(push, 1)     // 1️⃣ 정렬단위 1로 지정
struct Packed {
    char c;    // 1
    int i;     // 4
    short s;   // 2
};
#pragma pack(pop)

int main() {
    printf("%zu\n", sizeof(struct Normal)); // 2️⃣ 정렬된 구조체 크기
    printf("%zu\n", sizeof(struct Packed)); // 3️⃣ 패킹된 구조체 크기
    return 0;
}

🧠 해설 & 디버깅

  1. #pragma pack(push,1) → 구조체 안 멤버들을 1바이트 정렬. 패딩 없음.
  2. sizeof(struct Normal)
    → 정렬 단위: int(4),
    → 메모리: c(1) + padding(3) + i(4) + s(2) + padding(2) = 12 바이트
  3. sizeof(struct Packed) → 1바이트 정렬이라 padding 없음. 1+4+2 = 7 바이트.


🎯 한 줄 요약

트릭코드의 핵심
🧵스레드 트릭pthread_mutex_lock/unlock 또는 atomic 이용해 동기화
🧠메모리 모델 트릭atomic 연산 + memory_order 지정해 메모리 순서까지 제어
🧠구조체 정렬 트릭#pragma pack(n)로 정렬단위 조정하거나 멤버 순서 바꿔 크기 최적화

좋아요! 🧠
지금까지 보여드린 스레드 트릭 코드에서 2만(20000)이 나오는 과정을 정말 자세히 디버깅해 드릴게요.

우선 우리가 썼던 예제 코드를 다시 볼까요?


📂 코드 다시 보기

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 뮤텍스 초기화
int counter = 0;                                  // 공유자원

void* increment(void* arg) {
    for (int i = 0; i < 10000; i++) {             // 1️⃣ 10000번 루프
        pthread_mutex_lock(&lock);                // 2️⃣ 뮤텍스 잠금
        counter++;                                // 3️⃣ counter 증가
        pthread_mutex_unlock(&lock);              // 4️⃣ 뮤텍스 해제
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);  // 5️⃣ 스레드1 생성
    pthread_create(&t2, NULL, increment, NULL);  // 6️⃣ 스레드2 생성
    pthread_join(t1, NULL);                      // 7️⃣ 대기
    pthread_join(t2, NULL);                      // 8️⃣ 대기
    printf("%d\n", counter);                     // 9️⃣ 출력
    return 0;
}

🧠 흐름 디버깅하기

main()에서 pthread_create()를 2번 호출 → 스레드 2개 시작.

✅ 두 스레드가 거의 동시에 increment() 함수로 진입.


📍 increment 함수 내부 디버깅

각 스레드가:

  • i = 0 ~ 9999 동안 10000번 반복.

  • 한 번 돌 때마다:

    1. pthread_mutex_lock(&lock); → 다른 스레드가 이미 잠갔으면 대기.
    2. counter++ → 안전하게 1 증가
    3. pthread_mutex_unlock(&lock);

이게 2개의 스레드에서 각각 10000번 = 총 20000번 증가!


🧠 왜 딱 20000이 되냐고?

  • 뮤텍스(lock) 덕분에 두 스레드가 counter를 한 번에 하나씩만 수정.

  • 그러므로:

    • 스레드1: counter 0→10000까지 증가
    • 스레드2: counter 10000→20000까지 증가
  • 결국 출력 값은 20000.


🤔 뮤텍스가 없었으면?

뮤텍스 없으면 아래와 같은 레이스 컨디션 발생:

스레드1: counter 읽음(예:100) → +1 계산(101) → 쓰기
스레드2: counter 읽음(예:100) → +1 계산(101) → 쓰기

→ 한쪽 증가가 덮어씌워져 최종적으로 1만 안 오를 수 있음!
→ 이게 바로 레이스 컨디션!


🎯 핵심 요약

  1. 두 스레드가 각자 10000번 증가 → 2 * 10000 = 20000
  2. pthread_mutex_lock() 덕분에 한 번에 하나만 counter 수정 → 안전
  3. 따라서 출력값은 반드시 20000.

와, 정말 좋은 질문이에요! 🎯
지금까지 구조체 정렬(메모리 정렬) 이야기할 때 **padding(패딩)**을 자꾸 언급했는데,
이 부분을 아주 기초부터 확실히 이해할 수 있게 정리해 드릴게요.


🧠 패딩(Padding) 이란?

padding은 구조체 안에서 **메모리를 정렬(alignment)**하기 위해 컴파일러가 자동으로 채우는 여분의 바이트들을 말해요.


💡 왜 패딩이 필요할까?

  1. CPU가 메모리를 읽을 때, **정해진 단위(2, 4, 8바이트)**에 맞춰 읽으면 훨씬 빨라요.
  2. 정렬 안 하면 한 멤버를 읽는데 두 번 메모리 접근해야 하므로 비효율적.
  3. 그래서 컴파일러가 중간중간 빈 칸(padding)을 넣어서 멤버들의 시작 주소를 “딱 맞춰” 주는 거예요.

🧠 패딩이 계산에 어떻게 추가되나?

예를 들어 구조체:

struct S {
    char c;   // 1 byte
    int i;    // 4 bytes
    short s;  // 2 bytes
};

🧠 메모리 배치 계산

필드크기시작 주소패딩 유무
c10이후 int 정렬 위해 padding 3 추가
padding31~3구조체 정렬 위해
i44이제 4의 배수 주소에 놓임
s28이후 구조체 끝 정렬 위해 padding 2 추가
padding210~11구조체 크기가 4의 배수(12)되도록

🧠 크기 계산

  • c: 1
  • padding: 3
  • i: 4
  • s: 2
  • padding: 2
    총 크기 = 12 바이트

🧠 정리

✅ padding 자체는 프로그램이 쓰는 값이 아니고, 오직 메모리 정렬을 위해 삽입된 바이트.
✅ 이 바이트들에는 아무 의미 있는 값이 없음 → 초기화 안 하면 쓰레기 값.
✅ 계산에 포함되는 이유 = 구조체의 sizeof()가 멤버들의 크기 합보다 커지는 이유!


🎯 비유하기

💡 책장 정리한다고 생각해 보세요:

  • 큰 책을 바닥에서 4cm 단위의 선반 위에만 놓을 수 있어요.
  • 작은 소설책을 놓았는데, 다음 큰 책은 꼭 4cm 간격 선반 위에 놔야 함.
  • 그러면 소설책 위에 비어 있는 공간(패딩)이 남아요.
  • 선반 위 물건들 정리하기 위해 남긴 공간이 padding!

0개의 댓글