포트폴리오/[IoT 기반 피지컬 AI 교육일지]

[ C++ 네트워크 통신 ] 리눅스 멀티스레드(pthread)와 주요 소켓 옵션 및 기타 정리 (23일차 기록)

개발자혜콩 2026. 3. 20. 16:45

1. 오늘 한 것

① 멀티스레드: 리눅스 환경 (pthread)

  • 스레드 생성 (pthread_create): 리눅스(POSIX) 환경에서 스레드를 생성하는 핵심 함수 학습. <pthread.h> 헤더를 포함해야 함.
  • 스레드 종료 및 회수 (pthread_join): 실행 중인 스레드가 끝날 때까지 기다려주고, 스레드가 반환한 값을 받아 자원을 안전하게 회수하는 함수.
  • 스레드 일시 중지 함수: CPU 자원 배분을 위해 딜레이를 주는 함수들 학습.
    • sleep() : 초(Second) 단위 대기
    • usleep() : 마이크로초(Microsecond) 단위 대기
    • nanosleep() : 나노초(Nanosecond) 단위 대기 (주로 반도체, 초정밀 제어 등에서 사용)

② 멀티스레드 TCP 서버 실습 (bind 함수 구조 분석)

직접 코드를 짜며 bind()에 들어가는 sockaddr_in 구조체 세팅 과정을 뜯어봄.

 

  • memset(&serveraddr, 0, sizeof(serveraddr)); : 구조체의 쓰레기값을 0으로 깔끔하게 초기화.
  • serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); : 내 컴퓨터의 어떤 IP(유선, 무선, 로컬 등)로 들어오든 다 접속을 허용하겠다는 의미. (IP는 4바이트이므로 htonl 사용)
  • serveraddr.sin_port = htons(SERVERPORT); : 포트 번호 할당. (포트는 2바이트이므로 htons 사용)

 

③ 서버 안정성을 위한 핵심 소켓 옵션

  • SO_REUSEADDR (무조건 설정할 것!): 
    • 목적: TCP 서버를 강제 종료하고 다시 켰을 때, 포트가 아직 OS에 묶여있어 발생하는 bind() 에러(이미 사용 중인 주소)를 방지.
    • 효과: 기존에 사용 중인 IP 주소와 포트 번호를 강제로 재사용할 수 있게 해줌.
  • SO_LINGER:  
    • 목적: 소켓을 닫을 때 버퍼에 남아있는 데이터가 손실되지 않도록 보호.
    • 효과: 우아한 종료를 위한 shutdown() 함수와 함께 통신 프로토콜을 설계할 때 필수적으로 고려해야 하는 옵션.

 

④ C vs C++ 그리고 좋은 코드의 조건

  • C++은 약어가 많아 초기 진입 시 직관적으로 의미를 파악하기 어려울 수 있음.
  • 고급 개발자의 습관: 함수 안에 단일 기능을 명확히 담고, '이름만 봐도 무엇을 하는지 알 수 있게 짜면 구구절절 주석을 달 필요가 없음을 배움.

2. 문제 / 헷갈린 점

① pthread_create 인자 속  문법: void *(*start_routine) (void *)

  • 의문점: 스레드에 실행할 함수를 넘겨주는 부분인데, 왜 하필 void * (보이드 포인터)를 쓸까?
  • 깨달음: 이건 C언어의 '함수 포인터' 문법임. void *를 쓰는 이유는 "어떤 타입의 데이터(int, char, 구조체 등)가 들어올지 모르니, 무엇이든 다 받을 수 있는 만능 바구니(올라운더)로 만들어 두겠다"는 고급 설계 기법임. 이를 통해 스레드 함수를 아주 유연하게 재사용할 수 있음.

② pthread_join의 인자: 더블 포인터 (void **retval)

  • 의문점: ‼️포인터 하나도 벅찬데, 왜 별이 두 개(**)나 붙은 더블 포인터를 써야 할까?
  • 해결 및 보충: 스레드가 종료되면서 반환하는 값 자체가 포인터(주소)인 경우가 많음. 함수 안에서 '주소 값'을 변경해서 함수 밖(메인)으로 온전히 전달하려면, 그 '주소를 담을 변수의 주소'를 넘겨줘야 함. 그래서 주소의 주소, 즉 더블 포인터가 필요한 것. (이 부분은 메모리 구조를 그리면서 무조건 확실히 이해하고 넘어가기!)

③ 스레드의 CPU 독점 (고아 상태)

  • 상황: 멀티스레드 환경에서 하나의 스레드가 CPU 자원을 혼자 다 차지해버리면, 다른 스레드들이 멈추거나 무한 대기하는 현상이 발생함.
  • 해결책: usleep()이나 sleep() 같은 딜레이 함수를 전략적으로 배치하여, 쉴 틈을 주고 다른 스레드에게 CPU 점유율을 양보하도록(Context Switching) 조율하는 것이 핵심 노하우.

3. 오늘 배운 핵심

  • 리눅스 스레드의 시작과 끝: 생성은 pthread_create() (void 포인터 활용), 자원 회수는 pthread_join() (더블 포인터 활용)으로 쌍을 맞춰 관리해야 한다.
  • 서버 개발의 필수 방어막: 안정적인 서버 재시작을 위한 SO_REUSEADDR, 데이터 손실 방지를 위한 SO_LINGER 옵션은 소켓 프로그래밍의 기본 세팅이다.
  • 고급 개발자로 가는 길: 주석에 의존하기보다 함수명 자체를 직관적으로 짓고, void *를 활용해 재사용성이 높은 유연한 코드를 설계하자.

4. 다음 목표

  • 오늘 배운 bind() 구조와 리눅스 스레드 함수를 바탕으로, 실습 7-4(멀티스레드 TCP 서버) 코드를 백지상태에서 직접 타이핑하며 흐름 익히기.
  • 센서 데이터를 수집하는 클라이언트 프로그램이 비정상 종료되더라도, 서버를 재시작할 때 에러가 나지 않도록 소켓 설정에 SO_REUSEADDR 직접 적용해 보기.
  • 포인터와 더블 포인터 개념을 활용해 통신용 데이터 구조체(Packing) 메모리 구조 다시 한번 정리하기.