jun-wiki

View My GitHub Profile

Posts (Latest 10 updated) :
Read all
Contents:
  1. 스레드로 동시성
    1. 인자 사용 방식
      1. int* 단일 인자 방식
      2. client_arg_t* 구조체 인장 방식

오늘 TIL


스레드로 동시성

스레드 연결로 동시성 확보해야한다

실제 작동하는 오리진 서버는 단일이긴 하지만

스레드 프록시도 상당히 효과 있다고 한다

클라이언트들 ──(동시)──> [프록시: 스레드 N] ──(각자)──> 오리진 서버들

이런식의 흐름이라

프록시가 여러 연결을 동시에 붙잡고읽기/쓰기, DNS, connect 등을 겹쳐 해서 줄여준단다



대강 찾아보니 여긴 간단하다

그냥 pthread함수 써서 하면 되니 말이다

  • int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg)

    • thread: 생성된 스레드 ID가 써질 곳(출력용)

    • attr: 스레드 속성(스택 크기, detach 상태 등). 기본값 쓰려면 NULL

    • start_routine: 스레드가 실행할 함수(반드시 void* f(void*) 형태)

    • arg: start_routine에 전달될 단일 포인터(필요하면 구조체로 묶어서 전달)

    • 반환: 0 성공, 그 외 에러코드

  • int pthread_detach(pthread_t thread)

    • thread: 더 이상 join하지 않을 스레드를 “분리(detach)” 상태로

    • 효과: 종료 시 자원 자동 회수


걍 이 2개 함수로 해결 했다

함수 쓰는 건 위에 나온대로만 넣으면 되가지고 별로 할말이 없다

그 대신 사용하는 인자에 주의를 두어야 한다


인자 사용 방식

난 딱 두가지 알아보았다

사실상 과제에서는 차이가 없기에 더 짧은 거 쓰는게 이득이다


int* 단일 인자 방식

  • 메인 함수:
    Accept로 받은 connfdmalloc(sizeof(int))에 복사해 스레드에 전달해준다

  • 스레드 함수:
    *(int\*)로 값만 꺼내 쓰고, 그 포인터 한 번만 free, 처리 끝나면 Close(connfd)


// proxy_threads_intptr.c
#include "csapp.h"

void handle_client(int connfd);
void *thread_main(void *vargp);               // [THREAD] 프로토 타입 선언

int main(int argc, char **argv) {
    Signal(SIGPIPE, SIG_IGN);

    if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(1); }

    int listenfd = Open_listenfd(argv[1]);
    while (1) {
        struct sockaddr_storage clientaddr;
        socklen_t len = sizeof(clientaddr);

        int connfd = Accept(listenfd, (SA*)&clientaddr, &len);

        // [THREAD] 인자로 넘길 connfd를 힙에 “값 복사”
        int *arg = Malloc(sizeof(int));        // [THREAD] 포인터 메모리 할당
        *arg = connfd;                         // [THREAD] 연결 fd의 포인터로 지정

        pthread_t tid;                         // [THREAD]
        int rc = pthread_create(&tid, NULL, thread_main, arg); // [THREAD]
        if (rc != 0) {                         // [THREAD] 실패 시 누수 방지
            Free(arg);                         // [THREAD] 포인터 할당 메모리 해제
            Close(connfd);                     // [THREAD] 연결 닫기
        }
    }
    return 0;
}

void *thread_main(void *vargp) {               // [THREAD] 스레드 함수
    Pthread_detach(pthread_self());            // [THREAD] join 안하려고

    // [THREAD] int* 경로: 값만 꺼내 로컬에 보관
    int connfd = *(int*)vargp;                 // [THREAD] 스레드 전용 로컬 변수
    Free(vargp);                               // [THREAD] 인자 블록은 여기서 정확히 1회 해제

    handle_client(connfd);                     // main에서 이사옴 1
    Close(connfd);                             // main에서 이사옴 2
    return NULL;                               // main에서 이사옴 3
}

[THREAD] 주석 붙어있는 애들이 추가된 줄들이다

보다시피 그렇게 많이 바뀌지 않는다





client_arg_t* 구조체 인장 방식

  • 메인 함수:
    connfd + 클라이언트 주소를 구조체에 담아 스레드에 전달

  • 스레드 함수:
    구조체 포인터를 받아 필드 사용 후 그 구조체 포인터만 free

typedef struct {                                // [THREAD] 여러 값을 한 번에 안전히 넘기기 위한 구조체
    int connfd;                                 // [THREAD] 워커가 처리할 클라이언트 소켓 FD (소유권: 워커)
    struct sockaddr_storage addr;               // [THREAD] 클라이언트 주소(로깅/필터링 용도; 옵션)
    socklen_t addrlen;                          // [THREAD] 주소 길이
} client_arg_t;                                 // [THREAD] 구조체 이름


static void *thread_main(void *vargp) {         // [THREAD] POSIX 요구 시그니처: void* → void*
    Pthread_detach(pthread_self());             // [THREAD] join 안하려고

    client_arg_t *arg = (client_arg_t*)vargp;   // [THREAD] 메인에서 힙에 만든 인자 패키지 수신
    int connfd = arg->connfd;                   // [THREAD] 처리할 FD를 로컬 변수로 복사(소유권 명확화)
    free(arg);                                  // [THREAD] 인자 패키지 메모리 해제

    handle_client(connfd);                      // main에서 이사옴 1
    Close(connfd);                              // main에서 이사옴 2
    return NULL;                                // main에서 이사옴 3
}


int main(int argc, char **argv) {
    Signal(SIGPIPE, SIG_IGN);                         // [THREAD] 파이프 끊김으로 전체 프로세스 즉사 방지

    if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(1); }

    int listenfd = Open_listenfd(argv[1]);
    while (1) {
        struct sockaddr_storage clientaddr;
        socklen_t len = sizeof(clientaddr);           // [THREAD] Accept 전에 매회 길이 초기화(필수)

        int connfd = Accept(listenfd, (SA*)&clientaddr, &len);

        client_arg_t *arg = malloc(sizeof(*arg));     // [THREAD] 워커에 넘길 인자 패키지
        arg->connfd  = connfd;                        // [THREAD] 처리할 FD 지정
        arg->addr    = clientaddr;                    // [THREAD] 피어 주소 복사(원하면 로깅에 사용)
        arg->addrlen = len;                           // [THREAD] 주소 길이 저장

        pthread_t tid;                                // [THREAD] 생성된 워커의 ID를 받을 변수
        Pthread_create(&tid, NULL, thread_main, arg); // [THREAD] 워커 생성 → 메인은 즉시 다음 Accept로
    }
    return 0;
}

확연히 더 길어진 모습이다

기능은 동일하고 중간에 구조체가 개입한다는 차이가 존재한다

지금은 사실상 무의미하고

클라이언트 IP/포트 로깅, 접근 제어, 통계 등 부가정보가 필요할 때 이런식으로 사용하면 좋다