교수님: I'm gonna talk about threads today which is another very important building block. A computer scientist you're gonna hit up on a lot of this like multithreaded environment. So threads are very important building blocks for today's computing environment so you're better master how to create threads and how to use threads. So that's the goal of this lecture.
이재욱 교수님의 5월 31일 금요일자 명강의, 정리 레츠고💛
very similar to multiple process, but with threads instead of process(!)
일반적으로 프로세스는 Program Context(data register, stack pointer, program counter)와 Kernel Context(virtual memory structure, file descriptor table, brk pointer)를 가지고 있다. 그리고 메모리 구조도 평소 우리가 알고 있는대로, 맨 밑에는 read-only, 그 위에 read/write 부분, 맨 위에서부터 내려오는 stack, 중간에 shared library 구역, 밑에서부터 올라오는 heap region으로 나눠져있다.

여기서 stack pointer의 위치나 brk pointer 위치, 읽어들이는 pc 값, 프로세스 별로 생성되는 file descriptor table 등을 확인하고 넘어가보자.
Process = program context + code, data and stack
Process = main thread + code, data and kernel context
Thread 환경으로 오면 좀 달라진다. 가장 눈에 띄는 달라지는 점은 1) kernel context 가 shared로 바뀌었다. 2) thread 별로 stack이 분리되었다(!)

자기가 가지고 있는 stack pointer 그 위에 부분만 고려한다는 의미가 될 수 있겠고, 같은 프로세스 내의 다른 쓰레드라면 동일한 VM구조나, file descriptor table나 brk pointer을 공유한다는 뜻이다.
물론, 주의깊게 봐야할 점은 thread 별로 stack pointer, pc 값이 다르다는 점!

멀티쓰레드 환경에서는 각각 다른 stack pointer와 program counter를 가지고 있음을 알 수 있다. pc 값이 다른 건 당연한 거기도 하다. 같은 프로그램 내에서 일을 쪼개서 수행한다는 건 instruction을 분배한다는 거랑 같은 말이니까!

professor: each thread actually has its own instance of stack, pc.
이러한 부분이 distinguish from process 하는 부분이다 (?!)
🤔 그러면 여기서 의문이 들 수 있다. 분명 이전 슬라이드에서는 Process도 PC 값은 따로 가지는 거 아니었나? 왜 이게 차별화하는 부분으로 설명을 하셨지?
우리의 친구 챗지피티가 정리해준 내용을 함께 보자 챗지피티의 답변🤓
✅ 챗지피티를 통해 의문점에 대한 대답이 완전히 해소되었다. 그러니깐 프로세스도 프로세스 별로 pc를 따로 가지기는 하는데, 그 내부에서 pc 값들이 따로 따로 저장되는 게 아니라 그 뭉텅이 안에서 첫 instruction의 위치를 가진다는 뜻인 것 같고. 쓰레드는 같은 프로세스 내에서라도 다른 pc 값을 가진다는 거에서 차별점이 있다는 뜻!
쓰레드 정리
LWP (light weight process)that is there's only one file descriptor table, one socket table for all threads shared by a single process.
즉, 하나의 프로세스 내에 생성된 여러 개의 쓰레드는 하나의 파일 디스크립터 테이블이랑 하나의 소켓 테이블을 공유한다는 이야기.
같은 프로세스 내에서라도 다른 pc 값을 가질 수 있다. (pc)
스택을 따로 가져가서 함수 호출, 지역 변수 저장에 사용한다. (reg, stack)
참고로 리눅스 커널은 task 단위로 스케줄링을 한다고 한다. 그럼 task가 뭐냐? 프로세스도 될 수 있고, 쓰레드도 될 수 있고, 정의 상으로는 unit of activity라고 하네요~ 그래서 sequential process든 mutli-threaded process든 따로 차이를 두지 않는다고 합니다. 오호~
차이점 추가
가장 큰 차이는 아무래도,
Thread는 code, data 를 공유하는데 Process는 대체적으로 공유하지 않는다는 점
데이터를 공유한다는 건 양날의 검: 장점이기도 단점이기도
가장 큰 문제는 동기화 문제
Thread가 더 쌉니다. 아무래도..
Process는 자식 프로세스 만들고(create) 죽이고(reaping) 하는 과정들이 너무 비싸다.. Context Switching 맨날 해줘야 하는 것도 비싸다..
context switching 관련 추가.
같은 프로세스 내 쓰레드로 바꾸는 경우에는 pc 값 정도만 바꿔도 되는데
다른 프로세스의 쓰레드로 바꾸는 경우에는 마찬가지로 context switiching이 필요한 건 맞음.
사실 그리고 쓰레드끼리 바꾸는 것도 시간소요가 필요하긴 합니다.. 그래도 전자가 후자보다 시간이 덜 걸리기 때문에, os scheduler가 일을 할 때 가능하면 같은 프로세스 내 쓰레드로 바꾸는 걸로 스케줄링을 하면 좋겠죠. 캐시 미스도 줄일 수 있고 locality 이런 것도 다 고려해서~~ 뭐 암튼 그렇다고 합니다.
Interprocess Commuincation 관련 추가
IPC도 비싸고 시간이 더 오래 걸린다고 하네요.
Process Hierarchy vs Thread pool
프로세스는 부모 프로세스로부터 자식 프로세스를 만들고 또 죽이는데
쓰레드는 부모 자식 구분도 없고 서로가 서로를 죽일 수 있으며 심지어는 메인 쓰레드인데도 kill 당할 수 있는 평등(?)한 수평 구조! 메인 쓰레드는 특권이 있는 게 아니라 그냥 제일 먼저 실행된 쓰레드라서 메인이라고🫢

어쩌다보니 쓰레드와 프로세스 차이점이 너무 길어졌는데.. 꼭 다시 읽어보길!
자주 쓰이는 라이브러리로 아래 3가지가 있는데 Pthread의 P가 POSIX에서 따온 것이었다는게 흥미롭다. 우리 수업에서도 Pthread를 배운다!
pthread_create, ptherad_join 등이 있다. 예제와 함께 보는 게 이해가 훨씬 빠르다.
pthread_create 설명
int pthread_create(
pthread_t *thread,
pthread_attr_t *attr,
void* (*thread_routine)(void *),
void *arg)
thread id (생성된 쓰레드의 Id를 저장할 변수)thread attribute (usually NULL)thread routine (쓰레드가 실행할 함수)thread argument (세 번째 인자에 해당하는 함수의 인자로 전달될 값)fork()와 다른 점
pthread_create는 function pointer를 반드시 넘긴다. 쓰레드가 해야 할 일을 주는 것. fork()는 별다른 인수가 없고 그냥 부모를 복제함. 그리고 return 값이 child 일 때는 부모의 pid를 반환하고 부모일 때는 0을 반환함. int main() {
pthread_t tid;
// Create the thread
pthread_create(&tid, NULL, thread, NULL);
// Wait for the thread to finish
pthread_join(tid, NULL) != 0);
exit(0);
}
void *thread(void *vargp) {
printf("Hello World!\n");
return NULL;
}

실행 흐름 구조가 이런데, 눈여겨 봐야할 것은, 각각의 함수의 call 시점과 return 시점이다. 나중에 detached mode 등이 나올텐데, 그런 거 빼면 웬만해서는 joinable thread 라고 한다. join을 통해 하나로 합쳐지고 한번에 종료되고 자원이 회수된다.
이전 포스트에서 Iterative Echo Server랑 Process-Based Concurrent Echo Server를 다룬 적이 있었다. 프로세스는 무조건 fork()를 통해 클라이언트의 요청에 responsabilty를 가지는 자식 프로세스를 생성해서 처리했었다.
/// @brief main server routine accepting new connections. Creates a new
/// thread for every client and calls run_thread() on it.
/// @param listen_fd listening socket
void run_server(int listen_fd) {
while (1) {
int *client_fdp = malloc(sizeof(int));
struct sockaddr_in client; // Assuming IPv4 for simplicity
socklen_t clientlen = sizeof(client);
*client_fdp = accept(listen_fd, (struct sockaddr *)&client, &clientlen);
if (*client_fdp > 0) {
printf("Connection from "); dump_sockaddr((struct sockaddr *)&client); printf("\n");
pthread_t tid;
pthread_create(&tid, NULL, run_thread, client_fdp);
}
} else {
perror("accept");
free(client_fdp);
break;
}
}
}
Main Difference 1: malloc의 사용
// process-based model
client_fd = accept(listen_fd, &client, &clientlen);
// thread-based model
int *client_fdp = malloc(sizeof(int));
*client_fdp = accept(listen_fd, (struct sockaddr *)&client, &clientlen);
기존에는 그냥 선언해서 썼는데 왜 malloc을 굳이 사용했느냐? 답변은 추후 더 자세히 하겠시만, 간단하게 말해서 race condition을 방지하기 위함이다.
여러 쓰레드가 같은 주소의 client_fd를 참조하고 값을 업데이트 하다보면 쓰레드 간 인터리빙등의 문제로 인해서 잘못된 값이 들어갈 수 있어서, 아예 쓰레드 별로 메모리 공간을 다르게 할당해서 privite하게 사용될 수 있도록 한 거.
Main Difference 2: fork() -> pthread_create()
pthread_t tid;
pthread_create(&tid, NULL, run_thread, client_fdp);
hello world! 출력하는 예제에서는 pthread_create(&tid, NULL, thread, NULL);을 썼었던 거 기억하는가? 4번째 인수에는 해당하는 부분은 3번째 인수인 함수 포인터에 전달될 인수를 담는다.
/// @brief main thread routine
/// @param vargp pointer to socket fd
void* run_thread(void *vargp)
{
// get client_fd and free memory
int client_fd = *((int*)vargp);
free(vargp);
// detach the thread to allow self-reaping
pthread_detach(pthread_self());
// run receiver-sender loop
run_instance(client_fd);
// close the socket
close(client_fd);
return NULL;
}
void *vargp 설명
그냥 네이밍 컨벤션인듯 하다. 여기서는 client_fd 값임. 쓰레드에서 또 가져왔을 때 굳이 2개일 필요는 없으니깐 하나는 free 해주는 것.
pthread_detach(pthread_self()) 설명