[Linux] Thread (쓰레드)
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;
}
이렇게 하면 두 쓰레드가 같은 변수에 접근하면서 생길 수 있는 동시성 문제를 해결할 수 있다.