QUESTION
thread 함수가 return하면 모든 thread 자원이 즉시 사라지는가?
pthread_create는 libc thread descriptor, stack/TLS mapping을 준비하고 clone을 통해 같은 mm/files 등을 공유하는 kernel task를 만든다. start routine이 return하면 kernel execution은 끝나지만 joinable thread의 반환값과 일부 userspace metadata는 pthread_join까지 남는다.
pthread_t는 숫자 TID와 같은 API가 아니다. 종료 후 join/detach한 handle을 재사용하거나 비교 외 연산을 하는 코드는 구현 detail에 의존한다.
STRUCTURE
구조 그림
모든 thread는 heap과 global mapping을 공유하지만 stack, TLS, register, kernel task는 각각 따로 가진다.
CALL PATH
호출 흐름
kernel task 종료와 pthread library가 stack·descriptor를 재사용 가능한 상태로 만드는 시점이 다르다. joinable thread에는 반드시 한 명의 join owner를 둔다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| kernel/fork.c | copy_process(), copy_mm(), copy_files() | CLONE flag로 process와 thread 자원 공유를 결정 |
| kernel/exit.c | do_exit() | thread task 종료와 clear_child_tid 처리 |
| kernel/futex/core.c | futex_wake() | join 구현이 기다리는 TID clear wakeup 기반 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 -pthread thread_join.c -o thread_join01#include <pthread.h>
02#include <stdint.h>
03#include <stdio.h>
04#include <stdlib.h>
05
06struct job {
07 int begin;
08 int end;
09};
10
11static _Thread_local int jobs_done;
12
13static void *worker(void *argument)
14{
15 const struct job *job = argument;
16 long *sum = malloc(sizeof(*sum));
17 if (sum == NULL)
18 return NULL;
19 *sum = 0;
20 for (int value = job->begin; value < job->end; ++value)
21 *sum += value;
22 jobs_done++;
23 return sum;
24}
25
26int main(void)
27{
28 struct job job = { .begin = 1, .end = 1000 };
29 pthread_t thread;
30 if (pthread_create(&thread, NULL, worker, &job) != 0)
31 return 1;
32 void *result;
33 if (pthread_join(thread, &result) != 0 || result == NULL)
34 return 1;
35 printf("sum=%ld main_tls=%d\n", *(long *)result, jobs_done);
36 free(result);
37 return 0;
38}
CODE NOTES
코드 조각별 설명
static _Thread_local int jobs_done같은 symbol 이름이라도 각 thread가 별도 instance를 가진다. worker의 증가가 main thread instance에는 반영되지 않는다.
const struct job *jobmain stack 객체의 주소를 넘긴다. pthread_join 전까지 main stack과 job 수명이 유지되므로 이 예에서는 안전하다.
long *sum = mallocworker stack local 주소를 반환하지 않고 join 뒤에도 유효한 heap object ownership을 main에 넘긴다.
pthread_create(&thread성공 시 pthread_t handle을 얻는다. pthread 함수는 보통 -1/errno가 아니라 오류 번호를 직접 반환한다.
pthread_join(threadthread completion과 synchronize하며 반환값을 받고 joinable thread의 library 자원을 회수한다. 같은 thread를 두 번 join하면 안 된다.
DETAILS
세부 동작
thread stack은 고정 크기 mapping이다
pthread 기본 stack 크기는 process resource limit와 implementation 설정에서 정해진다. 큰 local array나 깊은 recursion은 process 전체 heap과 별개로 stack guard page에 닿을 수 있다.
pthread_attr_setstacksize로 바꿀 때 PTHREAD_STACK_MIN과 alignment, 실제 frame 사용량을 측정한다.
detach는 background ownership 선언이다
detached thread는 종료 시 자동 회수되어 join할 수 없다. detached로 바꾼 뒤 caller가 넘긴 context를 먼저 free하면 use-after-free가 생긴다.
shutdown 때 완료를 기다려야 하는 worker라면 joinable 상태와 명시적 join owner가 더 단순하다.
process exit와 thread exit를 구분한다
worker가 return하거나 pthread_exit를 호출하면 해당 thread만 끝난다. 어떤 thread든 exit()를 호출하면 exit_group으로 전체 process가 종료된다.
main return도 exit와 같으므로 다른 thread를 계속 실행하려면 main에서 pthread_exit를 쓰거나 join 구조를 둔다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
pthread descriptor | pthread_create에서 생기고 join/detached exit 뒤 재사용·해제된다 | pthread_t, result, cancel state |
thread stack/TLS | create 때 mapping/할당되고 thread 종료 cleanup 뒤 회수된다 | guard, stack size, TLS dtors |
task_struct | clone에서 생기고 thread do_exit/release에서 해제된다 | TID, clear_child_tid, robust list |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| thread 수가 계속 늘어남 | joinable thread 미join | /proc/PID/task와 stack mappings |
| worker 반환값 crash | worker stack 주소 반환 | allocation owner와 lifetime |
| pthread_create EAGAIN | task limit, stack VA/memory 부족 | ulimit -u, pids cgroup, maps |
LAB
직접 확인
- join을 제거해 반복 thread 생성 시 /proc/PID/status Threads와 memory 사용 변화를 본다.
- pthread_attr_getstack으로 worker stack base/size를 출력하고 /proc/PID/maps와 맞춘다.
- 여러 thread에서 _Thread_local 변수 주소를 출력해 서로 다른 TLS instance인지 확인한다.
./thread_joinstrace -f -e trace=clone3,clone,futex,set_robust_list,exit ./thread_joinPRIMARY REFERENCES