System Programming: Thread

전창우·2024년 11월 30일

system programming

목록 보기
7/9

what is Thread?

thread는 무엇일까? 프로세스보다 가벼운 경량화된 프로세스라고 생각할 수 있지만, Thread는 부모 프로세스와 메모리 영역을 공유한다는 점에서 자식 프로세스와 다르다.
또한, thread는 thread 간에도 메모리 공유가 일어난다. 이로 인해, 다양한 문제점이 발생할 수도 있다.

자식 프로세스는 부모 프로세스로부터 메모리 영역을 복제하므로, 부모 프로세스와 독립된 메모리 공간을 가지게 된다.

thread func

c에서 thread를 다루기 위한 함수들을 알아보자.

thread 생성

pthread_create work:새로운 thread를 생성하여 지정한 함수(thread 함수)를 실행

pthread_create func origin

#include <pthread.h>
int pthread_create (pthread_t  *thread,
                    pthread_attr_t *attr,
                    void *(*func)(void *),
                    void *arg
                    );

ARGS
pthread_t *thread: 생성된 스레드의 ID를 저장할 변수.

const pthread_attr_t *attr: 스레드 속성을 지정하는 포인터(기본 속성은 NULL)

void (start_routine)(void *): 스레드가 실행할 함수로, 스레드 함수는 void* 형이어야 함.

void *arg: 스레드 함수에 전달할 인자(없을 경우 NULL, 여러 값을 전달하고자 하는 경우구조체 포인터를 사용) 반드시, void* 형이어야 하며 스레드 함수에서 이를 사용할 때, 명시적 형변환을 통해 사용 가능하다.

pthread_join()

pthread_join work: thread가 종료될 때까지 대기하는 함수이다. 해당 thread를 호출한 부모 프로세스(thread)는 이 thread가 종료될 때까지 block 상태로 대기시키기 위해 사용한다. pthread_join을 통해 thread가 return 한 값을 void**형으로 받을 수 있다

pthread_join func origin

#include <pthread.h>
int pthread_join (pthread_t thread, void **retval);

ARGs
thread : 기다릴 thread's Id -> pthread_create의 인자로 받음
retval : 스레드 함수의 반환값

예제
아래 예제는 부모가 가지고 있는 count 객체에 대한 메모리 공유가 스레드에 어떻게 이루어지고 있는 지 보여주기 위해 만든 것으로, 출력값이 기대와 다른 것이 당연하다.

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

void *print_count(void *c);

int main()
{
	pthread_t t1; //생성한 thread의 id를 저장
    pthread_t t2;
    int count=0; 
    
    //thread func의 argument는 void*로 넘겨주어야 함.
    pthread_create(&t1, NULL, print_count, (void*)&count); 
    pthread_create(&t2, NULL, print_count, (void*)&count);

    pthread_join(t1, NULL);
    pthread_join(t2,NULL);
    
    return 0;
}
void* print_count(void* c)
{
    int* count = (int*) c; //명시적 형변환
    for(int i=0; i<5; i++)
    {
        printf("%d\n", ++(*count));   
    }
}
/*
output:
1
3
4
5
6
2
7
8
9
10
*/

Mutex

thread는 부모 프로세스(thread)와 또 다른 thread 간에 메모리 공유를 한다. 하지만, critical section(임계 영역)에서는 동시에 여러 thread가 하나의 공유 자원에 접근 하는 것을 허용하지 않아야 한다. 이는 공유 자원을 접근할 시 하나의 thread만을 접근하도록 하는 Mutex의 동기화 기법으로 해결 가능하다.

mutex func

대기 중인 thread는 lock(key)를 획득할 때까지 대기해야 하며, lock을 획득해야지만, 공유 자원(임계영역)에 접근할 수 있다.

mutex를 사용하면 프로그램의 속도가 저하되므로 최대한 적은 임계영역을 선정하자.
아니면, 각각의 thread가 자신의 counter 변수를 사용한 다음, main에서 그 변수를 처리하여 사용하는 게 효율적이다.

pthread_mutex_init origin

pthread_mutex_t mutex;
if (pthread_mutex_init(&mutex, NULL) != 0) {
    perror("Mutex initialization failed");
}
// Or
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

work: mutex를 초기화
ARGs
mutex: 초기화할 뮤텍스 객체의 포인터
attr: mutex의 속성을 지정(기본 속성일 경우 NULL)
return
0 : 성공
그외 : err

pthread_mutex_destroy origin

int pthread_mutex_destroy(pthread_mutex_t *mutex);

work: mutex 객체를 free시킴
ARGs
mutex: free할 뮤텍스 객체의 포인터

pthread_mutex_lock origin

int pthread_mutex_lock(pthread_mutex_t *mutex);

work : 뮤텍스를 잠금(lock을 얻을 때까지 대기) -> 간단하게 임계영역의 시작 부분에 적어주면 됨

pthread_mutex_unlock origin

int pthread_mutex_unlock(pthread_mutex_t *mutex);

work : 뮤텍스를 잠금을 해제(lock 반납) -> 간단하게 임계영역의 끝 부분에 적어주면 됨

예제

아래는 이전의 예제에서 critical section에 대해서 mutex를 적용시킨 것이다.

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

void *print_count(void *c);
pthread_mutex_t conuter_mutex= PTHREAD_MUTEX_INITIALIZER;//MUTEX 초기화

int main()
{
    pthread_t t1; //생성한 thread의 id를 저장
    pthread_t t2;
    int count=0; 
    pthread_create(&t1, NULL, print_count, (void*)&count);
    pthread_create(&t2, NULL, print_count, (void*)&count);

    pthread_join(t1, NULL);
    pthread_join(t2,NULL);
    pthread_mutex_destroy(&conuter_mutex); //free mutex
    return 0;
}
void* print_count(void* c)
{
    int* count = (int*) c;
    for(int i=0; i<5; i++)
    {
        pthread_mutex_lock(&conuter_mutex); //lock을 받을 때까지 대기
        printf("%d\n", ++(*count));   
        pthread_mutex_unlock(&conuter_mutex); //lock을 넘김
    }
}

Thread 통신

A 과자를 만드는 '생산자'와 A 과자를 소비하는 '소비자'가 있다고 가정하자.
만약 소비자가 제때 A 과자를 소비해주지 않는다면, 생산자는 A 과자를 더 이상 생산해줄 필요가 없다.
반대로, 생산자가 A 과자를 제때 생산해주지 않는다면, 소비자는 A 과자를 소비할 수 없다.

즉, 두 상황이 발생할 경우 각각의 역할을 하는 사람은 다른 역할을 하는 사람이 일을 처리할 때까지 기다려야 한다.

thread도 마찬가지이다.
다른 thread에서 작업을 처리해줘야 thread가 작업을 더 처리할 수 있을 때 어떻게 해야할까?

기다리면 된다.

pthread_cond_t와 pthread_cond_wait

pthread_cond_t는 조건 변수로, 다른 thread가 cond로 다시 signal을 보낼 때(자신의 작업을 완료할 때)까지 대기한다

pthread_cond_wait origin

int pthread_cond_wait( pthread_cond_t  *cond,
                       pthread_mutex_t *mutex );

pthread_cond_signal(pthread_cond_t *cond);

cond에 signal을 보내 해당 thread를 깨우는 함수
pthread_cond_signal origin

int pthread_cond_signal(pthread_cond_t *cond);

정리하면, 다음과 같은 순서로 동작한다.

  1. pthread_cond_wait 호출 시, 해당 cond를 다른 thread에서 호출할 때까지 뮤텍스를 해제하고 작업을 멈춘다(기다린다).
  2. 다른 스레드가 pthread_cond_signal로 신호를 보내면, 조건이 충족되었다고 판단하고 뮤텍스를 다시 받아 다시 작업을 시작한다.
  3. 반복

예제

아래 예제는, 이전에 소개한 과자 생산&소비 를 나타낸 코드이다.
공유 자원인 snacks에 대해서 mutex lock/unlock을 통해 관리한다.

하나의 예로, 생산자 thread가 과자를 초과 생산했을 때,
생산자 thread는 소비자 thread가 과자를 소비할 때까지 기다린다.
소비자 thread는 과자를 일정량 소비했다면, 생산자 thread를 다시 꺠운다.

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

#define MAX_SNACK 8

int snacks =0; //현재 생산된 과자의 수

pthread_mutex_t mutex  = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t produce = PTHREAD_COND_INITIALIZER;
pthread_cond_t consume = PTHREAD_COND_INITIALIZER;


void* consumer(void* arg)
{
    int consume_snacks = 0;
    for (int i = 0; i < 15; i++)
    {
        pthread_mutex_lock(&mutex); //cond_wait, cond_signal 모두 mutex_lock , unlock 내부에 작성

        while (snacks <= 0) { // 현재 과자가 0개면 wait
            pthread_cond_wait(&consume, &mutex);
        }

        printf("[소비자] %d번째 과자 소비 (현재 과자 개수: %d)\n", ++consume_snacks, --snacks);

        if (snacks <= 4) {
            pthread_cond_signal(&produce); //과자가 4개 이하면 producer 꺠우기 
        }

        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    return NULL;
}

void* producer(void* arg)
{
    int produce_snacks = 0;
    for (int i = 0; i < 15; i++)
    {
        pthread_mutex_lock(&mutex);

        while (snacks >= MAX_SNACK) { //과자가 최대치로 생성됐다면 wait
            pthread_cond_wait(&produce, &mutex);
        }

        printf("[생산자] %d번째 과자 생산 (현재 과자 개수: %d)\n", ++produce_snacks, ++snacks);

        if (snacks >= 4) { //과자가 4개 이상 생성됐다면 consumer 깨우기
            pthread_cond_signal(&consume);
        }

        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t t1;
    pthread_t t2;

    pthread_create(&t1, NULL, producer, NULL);
    pthread_create(&t2, NULL, consumer, NULL);

    pthread_join(t1,NULL);  
    pthread_join(t2,NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&consume);
    pthread_cond_destroy(&produce);

    return 0;
}

0개의 댓글