본문 바로가기
컴퓨터공학기초 개념/시스템 프로그래밍

33. 스레드 - 이해와 기본

by devraphy 2021. 9. 24.

1. Thread란?

a) 표준 thread API

- 리눅스의 스레드는 POSIX 스레드 또는 Pthread라고 부른다.

 

 

b) Pthread

- C언어로 구현된 유닉스 시스템의 핵심 스레딩 라이브러리다. 

- 저수준 API로 100여개의 함수를 제공한다.

- 다른 스레드 솔루션 또한 Pthread를 기반으로 구현되어 있으므로, 반드시 익혀야 하는 개념이다.


2. Pthread 라이브러리

a) 기본적인 사용법

- <pthread.h> 헤더 파일을 정의한다.

- pthread 라이브러리의 모든 함수는 pthread_ 로 시작한다.

- pthread 라이브러리의 함수는 두가지 핵심기능으로 나뉜다. 

  ▶ 스레드 관리 - 스레스 생성 / 종료 / 조인 / 디태치 함수 등

  ▶ 동기화 - 뮤텍스 등 동기화 관련 함수

 

 

b) Pthread 컴파일하기

- Pthread는 기본 라이브러리(glibc)에 포함되어 있지 않은 독립적인 라이브러리다.

- 그러므로 컴파일 과정에 있어서 다음과 같이 옵션을 붙여주어야한다. 

gcc -pthread test.c -o test

3. pthread 라이브러리 주요함수

a) 스레드 생성

- 스레스 생성에 사용되는 함수를 살펴보자.

// thread: 생성된 스레드 식별자
// attr: 스레드 특정 설정(기본 NULL)
// start_routine: 스레드 함수(스레드로 분기해서 실행할 함수)
// arg: 스레드 함수 인자 

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                   void *(*start_routine)(void *), void *arg);
                   
// 예시
pthread_t thread1;
void *thread_function(void *ptr);

ret = pthread_create(&thread1, NULL, thread_function, (void*)message1);

- 위의 예시코드를 기반으로 각 매개변수에 대해서 알아보자.

 

▶ 첫번째 인자(&thread1)

   - 스레드를 가리키는 포인터

   - 반드시 pthread_t 타입으로 선언이 되어야 한다. 

 

 

▶ 두번째 인자(NULL)

   - 스레드의 특정한 설정옵션

   - 일반적으로 NULL을 사용한다. 

 

 

▶ 세번째 인자(thread_function)

   - 해당 스레드가 수행할 함수의 이름

 

 

▶ 네번째 인자(message1)

   - 세번째 인자로 입력된 실행할 함수가 필요로하는 매개변수(인자)

 

 

▶ 반환값

   - 0은 스레드를 성공적으로 생성

   - 0이 아닌 경우 에러가 발생 

 

 

b) 스레드 종료

- 스레드 종료에 사용되는 함수를 살펴보자.

// exit과 유사하다
// NULL 또는 0을 매개변수로 입력하여 종료한다. 
void pthread_exit(void *retval);

// 예시
pthread_exit(NULL);

 

 

c) 스레드 조인

- 리눅스에서 프로세스를 생성하면, 기본적으로 메인 스레드가 생성된다. 

- 메인 스레드는 pthread_create()를 통해 추가적으로 생성된 스레드와 구분하기 위해 메인 스레드라고 지칭한다. 

 

▶ 스레드 조인을 사용하는 이유

   - 스레드 조인은 스레드의 동기적 처리를 구현하기 위해서 사용한다. 

   - 메인 스레드에서 pthread_create() 함수가 실행되어 추가 스레드가 생성된다. 

   - 추가적으로 2개의 스레드를 만든다고 할 때, 잠시 메인 스레드에서 실행할 코드는 멈추고 생성된 스레드의 작업을 진행한다.

   - 둘 중 하나의 스레드가 종료되면 다른 스레드가 종료되지 않아도 메인 스레드는 다음 코드를 실행하게 된다.

   - 즉, 비동기적 실행으로 인해 메인 스레드가 추가 스레드보다 빨리 종료될 수 있다. 

   - 이러한 비정상적인 동작을 막기위해서 스레드 조인을 사용해 동기적 처리를 구현한다. 

 

- 스레드 조인에 사용되는 함수를 살펴보자.

// thread: 기다릴 스레드의 식별자
// thread_return: 스레드의 반환값을 가져올 포인터 
int pthread_join(pthread_t thread, void **thread_return);

// 예시 - pthread 식별자를 가진 스레드의 종료를 기다리고, status 포인터로 종료값을 가져온다.
pthread_join(p_thread, (void *)&status);
printf("thread join: %d\n", status);

4. 스레드 생성/종료/조인 예제

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

void *thread_function(void *ptr) { // 생성된 스레드에서 실행될 함^_
        char *message;
        message = (char *) ptr; // ptr 매개변수를 message 변수에 넣는다.
        printf("%s \n", message); // message를 화면에 출력한다.
        pthread_exit((void *)100); // 스레드를 강제종료 후 상태값을 100으로 설정한다.
}

int main() {
        pthread_t thread1, thread2; // 스레드 포인터 변수 생성
        const char *message1 = "Thread 1";
        const char *message2 = "Thread 2";
        int ret, status;

        ret = pthread_create(&thread1, NULL, thread_function, (void*)message1);
        printf("%d\n", ret);
        if (ret == 0) { // 스레드1 생성 성공
                printf("pthread_create returns %d\n", ret);
        } else { // 스레드1 생성 실패
                printf("pthread_create returns error: %d\n", ret);
                exit(1);
        }
        ret = pthread_create(&thread2, NULL, thread_function, (void*)message2);
        if (ret == 0) { // 스레드2 생성 성공
                printf("pthread_create returns %d\n", ret);
        } else { // 스레드2 생성 실패
                printf("pthread_create returns error: %d\n", ret);
                exit(1);
        }
        pthread_join(thread1, (void **)&status); // 스레드 1이 끝날 때까지 대기 (무엇이 먼저 끝날지 모름)
        printf("thread1 returns: %d\n", status);
        pthread_join(thread2, (void **)&status); // 스레드 2가 끝날 때까지 대기 (무엇이 먼저 끝날지 모름)
        printf("thread2 returns: %d\n", status);
        return 0;
}

- 위의 결과화면을 보면, 오히려 스레드 2가 스레드 1보다 먼저 실행된 것을 볼 수 있다.

- 스레드 2가 먼저 실행되었고, 만약 먼저 끝났다 하더라도 스레드 1의 상태값이 먼저 출력된다.

- 그 이유는 마지막 부분에 작성된 스레드 조인으로 인해 스레드 1이 끝날때 까지 기다려야하기 때문이다. 

 

 

- 몇번 스레드가 먼저 종료되는지 궁금해서 마지막 부분의 스레드 조인 부분의 순서를 바꿔보았다.

- 스레드 2를 스레드 조인하여, 스레드 2가 끝날 때까지 기다리도록 하였다.

- 스레드 2가 먼저 실행되었고, 먼저 종료된다는 것을 확인할 수 있다. 

댓글