좋아요! 🎯 이번엔 더욱 심화된 트릭들로 갑니다.
이번 트릭들:
문제 형태 + 풀이까지 같이 가요!
다음 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이 아닌 예측 불가값 나올 수 있음.✅ 트릭: 레이스 컨디션 때문에 틀린 답 나올 수 있음.
다음 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은 원자성만 보장하지 순서 보장 없음 (메모리 모델 지정 필요)다음 코드를 보고 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은 원자성만 보장. 메모리 오더 지정 안 하면 동기화 순서 미보장
✅ 구조체 정렬 트릭 → 컴파일러 정렬 규칙(패딩) 때문에 예상보다 구조체 크기 커질 수 있음
좋아요! 🔥 지금까지 트릭 문제들 위주로 보여드렸는데, 이제 정말 기초부터 차근차근 이해하기 쉽게, 예제 위주로 하나씩 정리해 드릴게요.
✅ 하나의 프로그램 안에서 여러 작업을 동시에(병렬적으로) 수행하기 위해 쓰는 단위.
✅ 예: 두 사람이 동시에 수학 문제를 풀면 답을 빨리 구할 수 있는 것처럼, 프로그램 내부에서도 작업을 나눠서 수행.
int counter = 0;
void* increment(void* arg) {
for (int i = 0; i < 100000; i++) {
counter++; // <- 여기가 트릭 포인트!
}
return NULL;
}
counter++는 내부적으로 세 단계를 거쳐요:
✅ 여러 스레드가 동시에 위 단계를 섞어가며 하면?
mutex(뮤텍스)나 atomic(원자적 연산)을 써요.💡 두 사람이 한 공책에 숫자 쓰기:
✅ 컴파일러와 CPU가 프로그램 명령어를 어떻게 재배치하고 메모리 읽기/쓰기를 동기화하는지를 정한 규칙.
_Atomic int x = 0, y = 0;
void* t1(void* arg) { x = 1; return NULL; }
void* t2(void* arg) { y = 1; return NULL; }
atomic이라서 “읽고 쓰기” 자체는 안전하지만, 순서가 보장되지는 않아요.memory_order_seq_cst 등)를 지정하거나 mutex를 써야 확실히 순서를 보장.💡 여러 사람이 게시판에 글을 쓴다고 생각해요:
✅ 컴파일러가 구조체 안의 멤버를 CPU가 읽기 편하도록 메모리 정렬을 맞춰 배치하는 것.
struct S {
char c; // 1 바이트
int i; // 4 바이트
short s; // 2 바이트
};
| 멤버 | 크기 | 정렬 단위 |
|---|---|---|
| c | 1 | 4 (int 정렬) → 패딩 3 |
| i | 4 | 4 |
| s | 2 | 2 → 구조체 전체 크기를 정렬하기 위해 마지막에도 2 바이트 패딩 |
✅ 실제 메모리 구조:
c (1) + padding(3) + i (4) + s (2) + padding(2) = 12
💡 가방에 물건을 깔끔하게 정리할 때:
| 트릭 이름 | 뭐가 문제? | 어떻게 해결? |
|---|---|---|
| 스레드 트릭 | 여러 스레드가 한 변수 동시 접근 → 레이스 컨디션 | mutex나 atomic 연산 |
| 메모리 모델 트릭 | 동시 쓰기/읽기의 “순서”가 안 보장 | atomic + 메모리 오더 지정 또는 mutex |
| 구조체 정렬 트릭 | 구조체 안의 멤버들이 메모리 정렬되어 크기 커짐 | 멤버 순서 바꾸기 또는 #pragma pack(1) 사용 |
좋아요! 🎯
지금까지 언급한 트릭들(스레드 트릭, 메모리 모델 트릭, 구조체 정렬 트릭)에는 정해진 규칙과 문법이 있어요.
이해하기 쉽게 용어 정리 + 규칙 + 문법을 체계적으로 알려드릴게요.
pthread, mutex, atomicpthread = POSIX 스레드 라이브러리. 스레드 생성, 종료, 동기화를 도와줌.mutex(뮤텍스) = 한 번에 하나의 스레드만 임계구역(critical section)에 접근하게 만듦.atomic = 한 연산을 중단 없이 원자적으로(atomic하게) 수행.| 항목 | 문법 | 설명 |
|---|---|---|
pthread_create | pthread_create(&tid, NULL, func, arg); | 새로운 스레드를 생성 |
pthread_join | pthread_join(tid, NULL); | 스레드 종료 기다림 |
pthread_mutex_t | pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; | 뮤텍스 생성 |
pthread_mutex_lock/unlock | pthread_mutex_lock(&mtx); ... pthread_mutex_unlock(&mtx); | 크리티컬 섹션 보호 |
atomic<int> (C11) | #include <stdatomic.h> atomic_int counter = 0; counter++; | 한 연산을 안전하게 함 |
✅ 규칙 요약:
mutex, atomic, condvar)을 써야 안전.volatile, atomic, memory_orderC11 이후 표준에 의해 메모리 오더 지정 가능.
| 오더 옵션 | 의미 |
|---|---|
memory_order_relaxed | 원자성만 보장, 순서 없음 |
memory_order_acquire | 읽기 후 이전 연산들 보장 |
memory_order_release | 쓰기 전 이전 연산들 보장 |
memory_order_seq_cst | 전역 순서 일관성 보장(가장 강함) |
✅ volatile vs atomic
volatile: 컴파일러가 최적화하지 않도록 함 (메모리에서 매번 읽고 씀). 동기화 보장 없음.atomic: 원자적 연산 보장 + 메모리 모델 규칙 지정 가능.struct padding, #pragma pack구조체 멤버들은 **타입의 정렬 단위(보통 크기)**에 맞춰 메모리에 배치.
구조체 크기 = 가장 큰 멤버의 정렬 단위의 배수.
정렬을 줄이고 싶다면:
#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 또는 atomic | pthread_mutex_lock pthread_mutex_unlock |
| 메모리 모델 트릭 | atomic + 메모리 오더 지정 | atomic_int, memory_order_seq_cst |
| 구조체 정렬 트릭 | 정렬 단위에 맞춰 패딩 삽입 | #pragma pack(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;
}
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;int counter = 0;pthread_mutex_lock(&lock);pthread_mutex_unlock(&lock);pthread_create/pthread_joinprintf("%d\n", counter);원자적 연산 + 메모리 오더 지정하기!
#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;
}
atomic_fetch_add_explicit(..., memory_order_relaxed);memory_order_relaxed → 연산 순서 보장 없음.printf("%d\n", x); → 200000 출력.atomic vs volatileatomic: 연산 자체가 원자적 → 여러 스레드가 안전히 공유 가능volatile: 단순히 캐싱 방지 → 스레드 안전 보장 X정렬을 바꾸고 크기 변화를 관찰하기!
#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;
}
#pragma pack(push,1) → 구조체 안 멤버들을 1바이트 정렬. 패딩 없음.sizeof(struct Normal)int(4),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() 함수로 진입.
i = 0 ~ 9999 동안 10000번 반복.
한 번 돌 때마다:
pthread_mutex_lock(&lock); → 다른 스레드가 이미 잠갔으면 대기.counter++ → 안전하게 1 증가pthread_mutex_unlock(&lock);뮤텍스(lock) 덕분에 두 스레드가 counter를 한 번에 하나씩만 수정.
그러므로:
결국 출력 값은 20000.
뮤텍스 없으면 아래와 같은 레이스 컨디션 발생:
스레드1: counter 읽음(예:100) → +1 계산(101) → 쓰기
스레드2: counter 읽음(예:100) → +1 계산(101) → 쓰기
→ 한쪽 증가가 덮어씌워져 최종적으로 1만 안 오를 수 있음!
→ 이게 바로 레이스 컨디션!
pthread_mutex_lock() 덕분에 한 번에 하나만 counter 수정 → 안전와, 정말 좋은 질문이에요! 🎯
지금까지 구조체 정렬(메모리 정렬) 이야기할 때 **padding(패딩)**을 자꾸 언급했는데,
이 부분을 아주 기초부터 확실히 이해할 수 있게 정리해 드릴게요.
✅
padding은 구조체 안에서 **메모리를 정렬(alignment)**하기 위해 컴파일러가 자동으로 채우는 여분의 바이트들을 말해요.
padding)을 넣어서 멤버들의 시작 주소를 “딱 맞춰” 주는 거예요.struct S {
char c; // 1 byte
int i; // 4 bytes
short s; // 2 bytes
};
| 필드 | 크기 | 시작 주소 | 패딩 유무 |
|---|---|---|---|
c | 1 | 0 | 이후 int 정렬 위해 padding 3 추가 |
| padding | 3 | 1~3 | 구조체 정렬 위해 |
i | 4 | 4 | 이제 4의 배수 주소에 놓임 |
s | 2 | 8 | 이후 구조체 끝 정렬 위해 padding 2 추가 |
| padding | 2 | 10~11 | 구조체 크기가 4의 배수(12)되도록 |
✅ padding 자체는 프로그램이 쓰는 값이 아니고, 오직 메모리 정렬을 위해 삽입된 바이트.
✅ 이 바이트들에는 아무 의미 있는 값이 없음 → 초기화 안 하면 쓰레기 값.
✅ 계산에 포함되는 이유 = 구조체의 sizeof()가 멤버들의 크기 합보다 커지는 이유!
💡 책장 정리한다고 생각해 보세요: