CS/시스템 프로그래밍

[Linux] Thread (쓰레드)

honeyricecake 2022. 12. 25. 04:57

1. Thread란

 

Thread : 프로세스 내에서 독립적인, 별도의 컴퓨팅 단위

 

현재 우리 컴퓨터에는 수십개의 프로세스가 돌고 있는데, 프로세스가 실시간으로 바뀔 때마다 Context - Switching이 일어나면서 새로운 Process가 실행되는 내용을 불러온다.

(Context - Switching 이 반복되며 계속 실행하는 Process 가 바뀐다.)

 

그런데 Context - Switching이 생각보다 시간을 잡아먹는 행위이다.

그래서 Context - Switching에 드는 시간을 줄이는 것이 OS에서의 과제 중 하나였다.

 

Process를 계속 바꾸는 것이 시간을 많이 잡아먹으므로 상대적으로 작은 정보를 옮기면서 여러 일을 수행할 필요가 있었다.

그래서 생긴 것이 Thread이다.

 

간단한 예시 코드를 보자.

#include <stdio.h>
#inlcude <pthread.h>  // gcc에서는 헤더가 없는 경우에도 컴파일이 되는 경우가 꽤 있지만 이 헤더는 반드시 필요하다.

// 독립적인 실행하고 싶은 함수의 형식
// void* 을 리턴하여야 한다.
void* func(void* arg)
{
	for(int i = 0; i < 5; i++)
    {
    	printf("Thread Working\n");
        sleep(1);
    }
}

int main()
{
	pthread_t thrd1, thrd2; // 아이디가 있어야 한다.
    int result, status;
    
    result = pthread_create(&thrd1, NULL, func, NULL);
    // 첫번째 패러미터에 &thrd1을 넣어주면 생성된 쓰레드의 아이디가 변수에 담긴다.
    // 두번째 패러미터는 쓰레드의 속성을 지정, 디폴트로 NULL 사용 가능
    // 세번째 패러미터로 쓰레드로 실행할 함수의 함수포인터를 넘겨주어야 한다.
    // 네번째 패러미터는 넘겨줄 argument
    
    for(int i = 0; i < 10; i++)
    {
    	printf("Main works\n");
        usleep(500000);
    }
    pthread_join(thrd1, &status);
    // 1번 쓰레드가 끝나기 전 메인이 끝나면 1번 쓰레드가 끝나지 않고 프로그램이 종료 가능
    // 이를 방지하기 위해 1번 쓰레드가 끝나기를 기다리는 함수
    // 1번 쓰레드가 끝나고 결과는 status에 담긴다.
    
    return 0;
}

 

메인 함수와 쓰레드 함수가 동시에 실행되는 것을 확인할 수 있다.

 

(이를 컴파일할 때는 반드시 -lpthread 옵션이 필요하다. pthread라이브러리를 링크하여야 하기 때문이다.)

 

동작할 때의 흐름

 

main에서 pthread_create를 호출하면, 문제없이 돌아간다고 가정할 때 새로운 thread가 생긴다.

정상 동작시 pthread_create는 0을 리턴한다.

이 때 main함수는 main함수대로 thread함수는 thread함수대로 함께 실행된다.

 

Fork 한 것과 같이 새로운 Process 처럼 갈라지는 것 같지만 CPU, Register, Program Counter만 다를 뿐 나머지 프로그램의 내용은 같다.

 

Fork는 새로운 프로세스가 생성되어, 두개의 독립적인 Process가 실행되는 방식이다.

Fork시, 하나의 Process 내에 할당된 Virtual Memory (Code/ Static/ Stack/ Heap) 이 하나 더 생긴다.

하지만 Thread는 Code, Static, Heap 영역을 서로 공유하고, Stack 영역만 따로 쪼개서 쓴다.

 

Stack 영역만 서로 바꿔서 쓰면 되기 때문에 가볍게 동작시킬 수 있다.

 

다른 부분까지 전부 다 Context Switch 시켜가면서 costly하게 쓸 필요가 없다.

 

Multi-Core에서는, 두 개가 완전히 동시에 수행되어 속도 향상을 할 수 있는 가능성 또한 열려 이싿.

 

 

2. Thread 활용할 때 일반적인 방법

 

Main이 먼저 끝나면 Thread에 시켜놓았던 일이 다 끝나지 않고 종료될 수 있다.

그래서 보통 Thread를 쓸 때는 Main함수에서는 단순히 Thread룰 생성하는 일만 하고, 정보를 관리하면서 Thread가 끝나기를 기다리는 일을 주로 한다.

물론 두개의 Thread 가 서로 다른 일을 하게 만들 수도 있다.

 

ex. 소수의 개수를 세는 프로그램

 

Thread 를 쓰지 않는 기존의 prime_count program : 시간복잡도 -> O(sqrt(n))

위 문제에서 Multi Core로 두 쓰레드를 같이 계산하여 좀더 효율적으로 작업을 수행하도록 쓰레드를 사용하여 보자.

 

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

int N;

int isPrime(int n)
{
        for(int i = 2; i <= sqrt(n); i++)
        {
                if(n % i == 0) return 0;
        }

        return 1;
}

void* prime_count1(void* arg)
{
        int count = 0;
        for(int i = 2; i <= N / 2; i++)
        {
                if(isPrime(i))
                {
                        count++;
                }
        }

        // 쓰레드 종료후 stack 메모리는 할당이 해제되므로
        // heap에 동적할당 후 리턴해야함.

        int* ret = malloc(sizeof(int));
        *ret = count;

        pthread_exit(*ret);
}


void* prime_count2(void* arg)
{
        int count = 0;
        for(int i = N / 2 + 1; i <= N; i++)
        {
                if(isPrime(i))
                {
                        count++;
                }
        }

        int* ret = malloc(sizeof(int));
        *ret = count;

        pthread_exit(*ret);
}

int main()
{
        int count = 0;
        int status;
        pthread_t thrd1, thrd2;

        printf("Enter the num : ");
        scanf("%d", &N);

        pthread_create(&thrd1, NULL, prime_count1, NULL);
        pthread_create(&thrd2, NULL, prime_count2, NULL);

        pthread_join(thrd1, &status);  // 스테이터스에 결과값이 들어감.
        printf("%d\n", status);
        pthread_join(thrd2, &status);
        printf("%d\n", status);

        return 0;
}

이 때 status에는 결과값이 그대로 담기므로, func이 char*을 리턴한다면 status는 똑같이 char*이어야 한다.

즉, 리턴값과 자료형이 일치하면 된다.

 

3. Thread 와 동시성 문제 해결

 

지금까지 이렇게 동시에 작업을 수행하는 경우, 늘 동시성 문제를 고려해왔는데 쓰레드 역시 이를 피할 수 없다.

그리고 쓰레드의 동시성 문제를 해결하는 방법이 있는데 바로 Mutex-Lock이다.

 

Mutex - Lock은 상호배타적인 동기화 도구로 Mutual_Exclusive의 약자이다.

 

열쇠를 먼저 얻으면, 해당 Thread가 다시 열쇠를 놓을 때까지 다른 Thread는 해당 key를 가지는 mutex_lock block 내에 접근할 수 없고, 기다려야 한다.

 

pthread_mutex_lock(&mutex_key)를 통해 어떤 Thread가 먼저 Lock을 하면, pthread_mutex_unlock(&mutex_key)할 때까지 다른 Thread가 새로 lock을 걸 수 없다.

unlock될 때까지, pthread_mutex_lock부분에서 blocked된 상태에서 대기 상태에 들어간다.

lock - unlock 사이에 있는 코드가 동시성 이슈에서 보호된다.

 

어떤 thread에서 unlock을 해줘야 다른 thread가 열쇠 권한을 획득할 수 있다.

 

key를 정의할 때는, pthread_mutex_t 라는 type의 변수를 사용하여 초기화한다.

 

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

int N;
pthread_mutex_t count_lock;  // 뮤텍스 타입의 변수 선언
int total_count = 0;  // 전역 변수

struct PrimeTestRange
{
        int startN;
        int endN;
};

int isPrime(int n)
{
        for(int i = 2; i <=sqrt(n); i++)
        {
                if(n % i == 0) return 0;
        }

        return 1;
}

void* prime_count(void* arg)
{
        struct PrimeTestRange* pr = (struct PrimeTestRange*)arg;
        int count = 0;

        int startN = pr->startN;
        int endN = pr->endN;

        for(int i = startN; i <= endN; i++)
        {
                if(isPrime(i)) count++;
        }

        pthread_mutex_lock(&count_lock);
        total_count += count;
        pthread_mutex_unlock(&count_lock);
}

int main()
{
        int count, status;
        pthread_t thrd1, thrd2;

        struct PrimeTestRange r1, r2;
        printf("Enter the num : ");
        scanf("%d", &N);

        r1.startN = 2;
        r1.endN = N / 2;
        r2.startN = N / 2 + 1;
        r2.endN = N;

        pthread_create(&thrd1, NULL, prime_count, &r1);
        pthread_create(&thrd2, NULL, prime_count, &r2);

        pthread_join(thrd1, &status);
        pthread_join(thrd2, &status);
        printf("%d\n", total_count);

        return 0;
}

 

이렇게 하면 두 쓰레드가 같은 변수에 접근하면서 생길 수 있는 동시성 문제를 해결할 수 있다.