본문 바로가기
일상,취미

[TIL 36일차] 쓰레드 프로그래밍

by 진득한진드기 2024. 8. 15.

이전까지는 epoll 기반으로 서버를 구성해봤다.

전에 해봤던  멀티 프로세스 방식의 단점 보완되었다고 보면 된다.

개인적으로 생각하기에는 요즘은 쓰레드가 대세인 것 같다.


멀티쓰레드 기반의 서버


전체적인 모델을 멀티쓰레드로 가져갈건지 epoll로 가져갈건지 정해야 한다.

요즘 뭔가 쓰레드가 대세라 쓰레드가 더 좋은거 처럼 효율이 좋을거 같지만 실제로는 멀티 플렉싱 방법이 대체적으로 한 프로세스에서 돌아가기에 더 많이 사용된다.

그러면 왜 배우냐 싶은데 혼합되서 사용되는 윈도우의 IOCP에서 쓰레드가 사용되기에 알고가면 좋다.

원래 기존의 프로세스는 하나의 흐름을 전담했지만 쓰레드라는 개념이 나오면서 
프로세스는 쓰레드를 담는 바구니가 되었다.

하나의 프로세스 내에서 메인 함수를 호출해서 실행해갈수있는 쓰레드를 담는 바구니라고 생각하자.

메인 쓰레드를 생성하고 이후에 다른 쓰레드도 생성이 가능하고,

요청이 들어오면 프로세스를 생성해서 실행시켰던 것 처럼

쓰레드도 따로 연결요청을 처리해주는 것이 가능하다.

작업의 완료 시간과 처리 까지의 시간은 epoll이 더 빠르겠지만

여러개의 쓰레드가 작업해주므로 실시간성 만큼은 쓰레드가 더 우위에 있다.

간단하게 보면 경량화된 프로세스를 쓰레드라고 생각하면 된다.

쓰레드는 메모리 공간을 어느정도 공유하기 때문에 멀티 프로세스보다 통신이 좀 더 자유롭다.


가볍다 라는 것은 어떻게 알수있을까?


프로세스 생성에는 많은 리소스가 든다.
프로세스가 생성되면, 프로세스간의 컨텍스트 스위칭으로 인해서 성능이 저하된다. 컨텍스트 스위칭은 프로세스의 정보를 하드디스크에 저장 및 복원하는 일이다.


프로세스간 메모리가 독립적으로 운영되기 때문에 프로세스간 데이터 공유가 불가능하다. 따라서 운영체제가 별도로 메모리 공간을 대상으로 IPC 기법을 적용해야한다.


물론 쓰레드끼리도 컨텐스트 스위칭을 하지만 프로세스보다 빠르다.

 

이거는 어떻게 보면 당연하다고 볼 수 있는데, 프로세스는 문맥교환할 때 쓰레드보다 더 많은 데이터를 가지고 있기 때문에 데이터를 적재하는 과정이 더 많이 들기 때문이다.

 

그냥 머리속으로 택배를 물품을 옮겨볼때 같은 물품의 개수라도 각 물품의 무게가 다르면 작업처리 속도가 다른 것과 비슷하게 생각할 수 있지 않을까 싶다.

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int sum = 0;
void *thread_summation(void *arg){
    int start = ((int *)arg)[0];
    int end = ((int *)arg)[1];
    int i;
    for(i = start;i<=end;i++){
        sum += i;
    }
    return NULL;
}

int main(int argc,char *argv[]){
    pthread_t id_t1,id_t2;
    int range1[] = {1,5};
    int range2[] = {6,10};

    pthread_create(&id_t1,NULL,thread_summation,(void *)range1);
    pthread_create(&id_t2,NULL,thread_summation,(void *)range2);

    pthread_join(id_t1,NULL);
    pthread_join(id_t2,NULL);

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

 

 

위의 코드를 봐보자

 

기본적으로 제공해주는 pthread 에 조금 봐보자면 다음과 같다.

 

쓰레드를 생성하는 함수

int pthread_create(pthread_t  *restrict thread, const pthread_attr_t *estrict attr, void p(p_start_routine) (void p_), void *restrict arg);


성공 시 0, 실패 시 0 이외의 값 반환

thread : 생성할 쓰레드의 ID 저장을 위한 변수의 주소 값 전달, 참고로 쓰레드는 프로세스와 마찬가지로 쓰레드의 구분을 위한 ID가 부여된다.

attr : 쓰레드에 부여할 특성 정보의 전달을 위한 매개변수, NULL 전달 시 기본적인 특성의 쓰레드가 생성된다.

start_routine : 쓰레드의 main 함수 역할을 하는, 별도 실행흐름의 시작이 되는 함수의 주소 값(함수 포인터) 전달.

arg : 세 번째 인자를 통해 등록된 함수가 호출될 때 전달할 인자의 정보를 담고 있는 변수의 주소 값 전달.


쓰레드생성하고 나서 시작되는 함수를 인자로 줘야한다.

컴파일 떄는 -lpthread를 붙여줘야


특정 쓰레드 대기하기
pthread_join(pthread_t thread, void dp_status);


성공 시 0, 실패시 0의외의 값 반환

thread  : 매개변수에 전달되는 ID의 쓰레드가 종료될 때까지 함수는 반환하지 않는다.
status : 쓰레드의 main 함수가 반환하는 값이 저장될 포인터 변수의 주소 값을 전달.

첫 번째 인자로 전달되는 ID의 쓰레드가 종료될 때까지, 이 함수의 호출한 프로세스를 대기상태에 둔다.



안전함수 불안전함수


- 둘 이상의 쓰레드가 동시에 호출하면 문제를 일으키는 함수를 가리켜 쓰레드에 불안전한 함수(Thread-unsafe function)이라 한다.
- 둘 이상의 쓰레드가 동시에 호출을 해도 문제를 일으키지 않는 함수를 카리켜 쓰레드에 안전한 함수 (Thread-safe function)라 한다.

struct hostent * gethostbyname(const char * hostname); // 불안전

struct hostent *gethostbyname_r(const char *name, struct hostent *result, char *buffer, intbuflen, int *h_errnop);
// 안전



함수를 호출할때 별도의 메모리공간을 마련해서 호출하기에 안전하다.

헤더 파일 선언 이전에 __REENTRANT 를 정의 하면 쓰레드에 불안전한 함수의 호출문을 쓰레드에 안전한 함수의 호출문으로 자동 변경 컴파일 된다.

gcc -D __REENTRANT mythread.c -o mthread -lpthread 로 컴파일 하면된다.


워커 쓰레드 모델


각각 A,B의 일꾼을 만들어 일을 시키고 반환값을 받을때 까지 일시정지 한다.

그리고 그 결과를 취합하는 형태의 쓰레드 모델을 워커 쓰레드 모델이라고 한다.

 

임계영역

공유가 가능한 변수에 동시에 접근했을 때 문제가 발생한다.

실제로 싱글코어 기준으로 보면 CPU가 RAM에서 데이터를 가져와서 처리할텐데

두 쓰레드가 같은 변수에 동시에 접근한다고 할때 i라는 변수를 둘다 ++ 해준다고 하자

그러면 한 쓰레드가 먼저 i를 CPU 레지스터에 저장하고 ALU를 이용해서 1을 ++ 해준다.

그리고 다시  1을 올려준 i을 RAM에 올려주는 흐름이 일반적이다.

RAM에 올리려고하는데 그 순간 실행의 흐름이 다른 쓰레드로 넘어가면(컨텍스트 스위칭) 별도의 메모리 공간에 이 i값을 저장해놓는다.

그러면 바뀐 쓰레드는 i를 다시 RAM에서 가져와서 증가 시킨다. 그러고 RAM에 올려놓는다.

다시 실행 흐름이 넘어와서 기존의 쓰레드가 RAM에 다시 올리는 과정을 실행하면 값을 증가 하는것이 아니라 기존의 가지고 있던 1로 덮어버린다.

여기서 말하는 임계영역은  둘 이상의 쓰레드가 동시에 실행하면 문제를 일으키는 영역을 의미한다.  서로 다른 문장임에도 불구하고 동시에 실행이 되는 상황에서도 문제는 발생할 수 있기에 임계영역은 다양하게 구성된다.

이거는 내가 전에 올렸던 글에도 비슷한 글이 있는데 이처럼 중간에 특정 자원에는 접근을 제한 하는 방식의 락(lock)에 대해서 적은적이 있다. lock에 대해서는 정말 많은 글들이 있으니 한번 봐보는 것도 좋을 것 같다.