[UNIX] 멀티 스레드 (Multi Thread) 프로그래밍
- -
개념
프로세스 내에서 실행되는 흐름을 말한다.
기본적으로 하나의 프로세스에는 하나의 스레드가 실행되지만,
여러 개의 스레드를 생성할 수 있으며, 이는 동시에 여러 개의 작업을 수행할 수 있다.
이를 멀티 스레드(다중 스레드)라고 한다.
같은 프로세스의 스레드들은 코드, 데이터, 파일, 힙 등의 메모리를 공유한다.
하지만 자신만의 고유한 스레드 ID, 프로그램 카운터(PC), 레지스터 집합, 스택을 가진다.
단일 컴퓨팅 칩에 여러 코어를 배치하는 다중 코어 시스템에서는
처리 능력을 향상시키도록 다수의 CPU 집중 작업을 병렬로 처리할 수 있다.
하지만, 단일 코어 시스템에서는 멀티 스레딩 프로그램을 동시성으로 작업할 수밖에 없다.
멀티 스레드의 활용의 예로 게임 로딩 창을 구현할 때,
데이터들을 로드하고 세팅하는 스레드와, 로딩 창의 진행 바를 실시간으로 업데이트 하는 스레드가 나뉠 수 있다.
또한 웹 사이트를 구현할 때,
서버에서 많은 데이터를 받아오는 스레드와, 일반적인 UI를 그려주는 스레드가 따로 나뉠 수 있다.
프로그래머는 스레드를 생성하고 관리하기 위해 스레드 라이브러리를 이용한다.
본 포스팅은 UNIX 기반 운영체제에서 지원되는 POSIX 표준의 pthread를 다룬다.
pthread는 POSIX가 정한 스레드 동작에 대한 명세일 뿐이지, 구현체는 아니다.
따라서 Linux나 MacOS에서는 pthread 명세를 따르며 나름의 내부 구현이 이루어져 있다.
스레스 생성
#include <pthread.h>
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
pthread_create()는 호출한 프로세스에서 새로운 스레드를 생성한다.
첫번째 인자 thread는 함수가 성공적으로 호출되면 저장되는 스레드 ID이다. 이는 이후 다른 pthread 함수에서 사용된다.
(pthread_t는 Linux에서 unsigned long이다.)
두번째 인자 attr은 스레드 생성된 스레드의 속성을 결정한다. NULL 입력 시 기본 속성을 제공한다.
(attr에 대한 더 자세한 정보는 메뉴얼 참고.)
세번째 인자 start_routine(함수 포인터)은 새 스레드가 해당 함수를 호출하며 실행을 시작한다.
네번째 인자 arg는 start_routine 함수의 유일한 인자로 전달된다.
성공시 0을 반환하며, 이외의 값은 에러다.
사용 예시는 아래와 같다.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
void* some_task(void* data)
{
printf("I'm New Thread : %d\n", *((int*)data));
return NULL;
}
int main()
{
pthread_t thread;
int data = 42;
int res = pthread_create(&thread, NULL, some_task, &data);
if (res == 0)
printf("I'm Main Thread\n");
else
printf("pthread_create failed\n");
return 0;
}
ptherad_create()의 인자에 넣어준 some_task()의 반환 값과 매개변수 타입은 void* 고정이며,
매개변수는 호출한 스레드에서 넘겨준 데이터의 포인터 타입으로 형변환, 역참조하여 사용한다.
some_task()의 반환 값은 이후에 설명할 pthread_join()에서 판별 가능하며,
함수 종료 상태에 따라서 특별한 정수를 내보낼 수도 있고, 동적할당 한 메모리를 내보낼 수도 있다.
멀티 스레드 제어
pthread_join
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
joinable한 스레드 생성 후, thread로 지정된 스레드가 종료될 때까지 기다린다.
이후 호출자는 대상 스레드에 할당된 메모리 또는 기타 리소스를 해제한다.
joinable한 스레드를 join하지 않은 경우, 좀비 스레드가 생성된다.
이는 일부 시스템 리소스를 소모하고, 좀비 스레드가 어느정도 누적될 경우 새 스레드를 만들 수 없게 된다.
모든 스레드는 해당 프로세스 내의 다른 스레드가 join할 수 있다.
retraval이 NULL이 아니라면, 위에 설명한 것과 같이 대상 스레드의 종료 반환값을 복사받는다.
스레드가 취소된 경우, PTHREAD_CANCELED가 복사된다.
성공시 0을 반환하며, 이외의 값은 에러다.
해당 스레드가 이미 종료된 경우 즉시 반환된다.
사용 예시는 아래와 같다.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#define SIZE 2
void* some_task(void* data)
{
if (*((int*)data) != 42)
return NULL;
return data;
}
int main()
{
pthread_t thread[SIZE];
int data[SIZE] = {42, 77};
void* result[SIZE];
for (int i=0; i<SIZE; ++i)
pthread_create(&thread[i], NULL, some_task, &data[i]);
for (int i=0; i<SIZE; ++i)
{
int res = pthread_join(thread[i], &result[i]);
if (res != 0)
{
printf("pthread_join error\n");
return 1;
}
if (result[i] == NULL)
printf("%d Thread : Invalid Data\n", i);
else
printf("%d Thread : Valid Data\n", i);
}
return 0;
}
두번의 반복문을 돌아 스레드를 만들고 이들을 다시 join하여 판별하는 예제다.
첫 번째 루프에서는 올바른 데이터를 넘겨줬고, 두 번째 루프에서는 올바르지 않은 데이터를 넘겨줬다.
실행 결과는 아래와 같다.
pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t thread);
꼭 스레드를 join하며 기다릴 필요는 없다.
pthread_detach() 함수는 thread로 분류된 스레드를 분리된 것으로 표시한다.
분리된 스레드는 종료될 시, 해당 스레드의 리소스가 자동으로 해제된다.
스레드가 분리되면 스레드를 다시 joinable하게 만들 수 없으므로 주의하자.
분리된 상태에서 시작하려면 스레드 생성 전 pthread_attr_setdetachstate()을 통해 attr로 설정해주면 된다.
성공시 0을 반환하며, 이외의 값은 에러다.
사용 예시는 아래와 같다.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#define SIZE 2
void* some_task(void* data)
{
usleep(100);
printf("Finished work!\n");
if (*((int*)data) != 42)
return NULL;
return data;
}
int main()
{
pthread_t thread[SIZE];
int data[SIZE] = {42, 77};
void* result[SIZE];
for (int i=0; i<SIZE; ++i)
{
pthread_create(&thread[i], NULL, some_task, &data[i]);
int res = pthread_detach(thread[i]);
if (res != 0)
{
printf("pthread_detach error\n");
return 1;
}
}
usleep(1000);
return 0;
}
스레드를 분리시켰다고 하더라도, 메인 스레드가 종료되면 남은 스레드들도 모두 종료되므로 주의하자.
pthread_cancel, pthread_kill
#include <pthread.h>
int pthread_cancel(pthread_t thread);
pthread_cancel()은 thread로 분류된 스레드에게 취소 요청을 보내는 함수다.
대상 스레드가 취소 요청에 반응할지의 여부는 스레드의 속성에 따라 달라진다.
pthread_setcancelstate() 나 pthread_setcanceltype() 참고.
#include <signal.h>
#include <pthread.h>
int pthread_kill(pthread_t thread, int sig);
pthread_kill()은 thread로 분류된 스레드에게 sig 의 시그널을 보내는 함수다.
시그널은 프로세스 전체에 적용된다.
다른 스레드에게 종료 시그널을 보낼 경우 프로세스 전체가 종료된다는 뜻.
스레드를 임의로 중지시키는 것에 대한 위험성
pthread_kill() 은 위의 이유로 위험하고,
pthread_cancel() 또한 단순히 중지만 시킬 것이 아니라 pthread_cleanup_push() 등을 이용하여
교착 상태 해결과 메모리 해제에 대해 신경써줘야 한다.
이와 관련된 더 자세한 내용은 본 포스팅에서 다루지 않는다.
대신 아래의 글에서 위험성과 해결법에 대해 설명한다.
멀티 스레드 데이터 공유
멀티 스레드가 공유 데이터에 동시에 접근할 경우 공유 데이터 값이 손상될 수 있다.
이러한 경쟁 조건이 발생할 수 있는 코드 영역을 임계영역이라 하며, 이 문제를 해결하며 데이터 공유를 진행해야한다.
이에 대한 자세한 설명은 아래의 링크 참고
참고 자료
https://en.wikipedia.org/wiki/Thread_(computing)
https://man7.org/linux/man-pages/man3/pthread_create.3.html
https://man7.org/linux/man-pages/man3/pthread_join.3.html
https://man7.org/linux/man-pages/man3/pthread_detach.3.html
https://man7.org/linux/man-pages/man3/pthread_cancel.3.html
https://man7.org/linux/man-pages/man3/pthread_kill.3.html
개인 공부용 포스팅인 점을 참고하시고, 잘못된 부분이 있다면 댓글로 남겨주시면 감사드리겠습니다.
'Operating System > UNIX' 카테고리의 다른 글
[UNIX] 멀티 프로세스 및 스레드 공유 데이터 동기화 (0) | 2023.12.12 |
---|---|
[UNIX] 파이프 (Pipe) 제대로 사용하기 (0) | 2023.12.04 |
[UNIX] 멀티 프로세스 (Multi Process) 프로그래밍 (1) | 2023.12.04 |
[UNIX] 입출력 및 리다이렉션 (I/O Redirection) (0) | 2023.11.20 |
[UNIX] I/O 멀티플렉싱 (Multiplexing) 및 select (0) | 2023.11.17 |
소중한 공감 감사합니다