Network/소켓통신(Linux)

멀티스레드: 리눅스 1

busy맨 2023. 11. 19. 22:18

2023.11.19 - [Network/소켓통신(Linux)] - 멀티스레드 개념

 

멀티스레드 개념

1. 스레드 기초 1) 소켓 응용 프로그램과 멀티스레드 멀티스레드를 사용하지 않는 경우 문제 두 개 이상의 클라이언트가 서버에 접속할 수 있으나, 서버가 동시에 두 개 이상의 클라이언트를 서

kmg0157.tistory.com

이전 게시글을 통해 개념 참고

 

 

1. 스레드 API

1) 스레드 생성과 종료

  • 리눅스에서 스레드를 생성할 때는 pthread_create() 함수 사용
#include<pthread.h>
int pthread_create(
	pthread_t *thread,
    	const pthread_attr_t *attr,
    	void *(*start_routine) (void *),
    	void *arg
)
// 성공 시 0, 실패 시 오류 코드 리턴
  • thread
    • 스레드 생성이 성공하면 스레드 식별자가 저장
  • attr
    • 생성할 스레드의 속성을 제어
  • start_routine
    • 스레드 함수의 시작 주소
  • arg
    • 스레드 함수에 전달할 인수

 

  • 스레드를 종료하는 방법
    1. 스레드 함수가 리턴
    2. 스레드 함수 안에서 phread_exit() 함수를 호출
    3. 다른 스레드가 pthread_cancel() 함수를 호출하여 스레드에 취소 요청을 보냄
    4. 메인 스레드가 종료하면 프로세스 내의 다른 모든 스레드가 강제 종료됨
  • 일반적으로 1,2 번 방식을 사용하는 것이 바람직함
#include<pthread.h>
int pthread_eixt(
	void *retval	//종료 코드	
)
#include<pthread.h>
int pthread_cancel(
	pthread_t thread	//취소할 스레드 식별자	
)

 

EX) 스레드 생성과 종료, 인수 전달 연습

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

struct Point3D
{
        int x, y, z;
};

void *MyThread(void *arg)
{
        sleep(1);
        Point3D *pt = (Point3D *)arg;
        printf("Running MyThread() %lu: %d, %d, %d\n",
                pthread_self(), pt->x, pt->y, pt->z);
        return NULL;
}

int main(int argc, char *argv[])
{
        int retval;

        // 첫 번째 스레드 생성
        pthread_t tid1;
        Point3D pt1 = { 10, 20, 30 };
        retval = pthread_create(&tid1, NULL, MyThread, &pt1);
        if (retval != 0) return 1;

        // 두 번째 스레드 생성
        pthread_t tid2;
        Point3D pt2 = { 40, 50, 60 };
        retval = pthread_create(&tid2, NULL, MyThread, &pt2);
        if (retval != 0) return 1;

        printf("Running main() %lu\n", pthread_self());
        sleep(2);
        return 0;
}

실행결과

  • 메인 스레드와 더불어 MyThread 두 개의 ID가 출력되고, 각 스레드에 전달한 Point3D 구조체 내용도 확인 가능

2) 스레드의 최대 개수

  • 32비트 리눅스에서 실행되는 모든 프로세스는 4GB의 가상 주소 공간(Virtual Address Space)을 각각 할당
  • OS 커널 영역을 제외하고 하위 3GB만 자유롭게 접근하여 사용할 수 있는 공간
  • 스레드를 하나 만들 때마다 하위 3GB에서 스택을 위한 공간을 할당 받음
  • pthread_create() 함수를 호출할 때 기본 스택 크기를 8MB라 가정하면, 3GB/8MB = 384가 단순 계산된 최대개수
  • 그러나 하위 3GB에는 실행 파일, 공유 라이브러리, 힙, 환경 변수 등을 위해 사용되는 영역이 존재
  • 따라서 생성가능한 스레드의 최대 개수는 더 줄어듬
  • 스레드를 필요 이상으로 많이 생성하면 메모리를 많이 소모할 뿐 아니라 스레드 간에 컨텍스트 전환 횟수가 증가하여 오히려 성능 저하

3) 스레드 제어

  • 스레드 우선순위 변경
    • 스레드는 리눅스 운영체제 내부(Kernel)로 들어가면 태스크(Task)라고 불리는 실행 단위와 일대일 대응
    • 프로세스 내부의 스레드 하나하나가 커널 내부의 태스크로 표현되며, 실질적인 실행 주체가 됨
  • CPU 스케줄링(Scheduling)
    • 각 태스크에 CPU 시간을 적절히 분배하기 위한 정책
      • 리눅스 운영체제에서는 항상 여러 태스크가 CPU 시간을 사용하기 위해 경쟁하기 때문에
    • 태스크 스케줄링, 프로세스 스케줄링 등으로도 불림
  • 우선 순위 결정 요소
    • 스케줄링 정책(Scheduling Policy)
      • 실시간(Real-Time)과 정규(Normal) 두 가지 정책
      • 실시간 정책을 따르는 태스크는 정규 정책을 따르는 태스크보다 항상 우선순위 높음
    • 스케줄링 우선순위(Schduling Priority)
      • 실시간 스케줄링 정책은 1(최저)~99(최고)범위의 우선순위 지원
      • 정규 스케줄링 정책은 고정된 순위를 지원하지 않으므로 우선순위를 0으로 간주

EX) 스레드 우선순위 변경 연습

  • nice() 를 호출하면 호출한 스레드의 우선 순위가 변경됨
  • 인수인 inc의 값은 -20~+19까지 가능
  • 리턴값인 Nice 값이 리눅스 시스템 콜의 실패를 뜻하는 -1을 가질 수 있음
  • nice() 호출 전에 전역 errno를 0으로 초기화하고, 호출 후에 리턴값과 errno값을 확인해야
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>

void *MyThread(void *arg)
{
        // 우선순위를 높게 설정한다.
        errno = 0;
        int retval = nice(-20);
        if (retval < 0 && errno != 0) {
                perror("nice() in MyThread");
                exit(1);
        }

        while (1) {
                write(1, ".", 1);
        }
        return 0;
}

int main()
{
        // CPU 개수를 알아낸다.
        int numberOfProcessors = sysconf(_SC_NPROCESSORS_ONLN);

        // CPU 개수만큼 스레드를 생성한다.
        for (int i = 0; i < numberOfProcessors; i++) {
                pthread_t tid;
                pthread_create(&tid, NULL, MyThread, NULL);
        }

        // 우선순위를 낮게 설정한다.
        errno = 0;
        int retval = nice(19);
        if (retval < 0 && errno != 0) {
                perror("nice() in main");
                exit(1);
        }

        sleep(1);
        while (1) {
                write(1, "*", 1);
        }
        return 0;
}

  • MyThread의 우선순위가 높으므로 대부분 '.' 이 출력되지만, 메인 스레드가 출력하는 '*'도 확인 가능
  • 스레드의 우선 순위가 고정되어 있지 않기 때문에 특정 스레드에 높은 우선순위를 부여하더라도
    낮은 우선 순위를 가진 스레드도 실행 기회가 주어짐

4) 스레드 종료 기다리기

  • 스레드가 생성되면 다른 스레드와 경쟁하며 독립적으로 실행됨
  • 때로는 한 스레드가 다른 스레드의 종료 여부를 확인해야 할 때가 있음
  • pthread_join() 함수를 사용하여 특정 스레드가 종료할 때까지 대기
#include<pthread.h>
int pthread_join(
	pthread_t thread	//종료를 기다릴 대상 스레드 식별자
    	void** retval	//스레드 함수의 리턴값을 받는데 사용
)

 

EX) 스레드 종료 기다리기 연습

#include <stdio.h>
#include <pthread.h>

int sum = 0;

void *MyThread(void *arg)
{
        int num = (int)(long long)arg;
        for (int i = 1; i <= num; i++)
                sum += i;
        return 0;
}

int main(int argc, char *argv[])
{
        int num = 100;
        pthread_t tid;
        pthread_create(&tid, NULL, MyThread, (void *)(long long)num);

        printf("스레드 실행 전. 계산 결과 = %d\n", sum);
        pthread_join(tid, NULL);
        printf("스레드 실행 후. 계산 결과 = %d\n", sum);
        return 0;
}

  • 스레드 실행 전에는 결과가 틀리지만, 실행 후에는 올바른 결과 출력

'Network > 소켓통신(Linux)' 카테고리의 다른 글

멀티스레드 : 리눅스 2  (1) 2023.11.19
멀티스레드 개념  (2) 2023.11.19
TCP 동작 과정 및 함수 정리  (0) 2023.11.19
데이터 전송하기  (0) 2023.09.17
TCP 서버 - 클라이언트 구조  (2) 2023.09.02