1. 개요
Thread / 스레드[1]운영체제의 실행 단위 중 하나로 프로세스보다 작은 실행 단위다. 현재 운영체제의 필수요소 중 하나.
2. 상세
예전에는 프로그램을 실행하는 흐름은 프로세스 뿐이었는데 소프트웨어가 진보하면서 하나의 프로세스에서 복잡한 동시 작업을 요구하기 시작하였다. 이를 위해 하나의 프로그램이 여러 개의 프로세스를 생성하여 서로 통신하는 식으로 구현을 하게 되었는데 프로세스 특성상 이러한 동시 작업을 수월하게 할 수 없었다.[2]그러다보니 프로세스보다 작은 실행 단위가 필요하였고 그게 바로 스레드다. 하나의 프로세스에서 여러 개의 스레드가 메모리를 공유하여 코드를 실행한다. 프로세스와 달리 생성과 제거가 빠르고 작은 메모리를 점유하고 정보 교환이 쉽고 Context Switching[3] 부하가 적다는 장점을 가지고 있지만. 그 대가로 자원 선점과 동기화 문제를 가지게 되었다.
대다수의 운영체제의 스케줄러는 스레드를 최소 단위로 하여 동작한다. 즉 스레드가 코드 실행을 담당한다.
3. 사용법
#!syntax cpp
#include <pthread.h> // pthread_t, pthread_create(), pthread_join() ...
void* func0000(void* args0);
void* func0001(void* args1);
int main(void){
pthread_t tid[2];
int args[2];
int status;
pthread_create(&tid[0], NULL, func0000, (void*)&args[0]);
pthread_create(&tid[1], NULL, func0001, (void*)&args[1]);
pthread_join(tid[0], (void**)&status);
pthread_join(tid[1], (void**)&status);
return 0;
}
▲ POSIX Thread (pthread)를 사용하여 두 개의 스레드를 만들고, 각각의 스레드가 서로 다른 함수를 실행하게 만드는 프로그램.[4]
4. 스레드와 프로세스의 차이점
프로세스: 각각의 은행 지점
스레드: 은행 지점 하나에 속한 고객 창구 여러 개
프로세스는 서로 완전히 독립된 메모리 공간을 가진다. 각 프로세스는 자신만의 스택과 데이터 영역을 가지고 보호를 받는다. 따라서 프로세스는 실행될 때 운영체제는 PCB[5]와 메모리 공간을 할당하고 초기화시키는 과정을 거친다.스레드: 은행 지점 하나에 속한 고객 창구 여러 개
각 프로세스의 메모리 영역은 완전히 독립되어 있으므로 한 프로세스가 다른 프로세스의 영역을 들여다볼 수 없다. 따라서 프로세스끼리 정보를 주고 받으려면 프로세스간 통신을 활용하거나 공유 메모리를 만들어서 데이터를 주고 받는 등의 번거로운 방법을 사용해야 한다. 은행 지점을 하나 새로 세우려면 부동산을 계약하고 내부 인테리어 공사를 진행하며, 은행 지점끼리 연락하기 위해서는 사람이 직접 가거나 전화를 해야 하는 상황을 생각해보면 된다. 반면 한 프로세스가 오류가 발생하여 비정상적으로 종료되더라도 다른 프로세스에는 영향이 거의 없다.[6] 은행 지점 하나가 화재 등으로 손상되어도 다른 지점에서 업무를 볼 수 있는 것과 비슷하다.
반면 스레드의 경우 스택은 각자 가지고 있지만 자신이 소속한 프로세스의 메모리 영역을 공유한다. 따라서 프로세스의 메모리 영역에 자유롭게 접근할 수 있어 보다 쉽고 빠르고 편하게 정보 교환이 가능하다. 같은 은행 지점에서는 옆 창구에 말만 하면 바로 알아들을 수 있는 것과 비슷하다. 단, 스레드 하나가 잘못된 연산이나 버그 등으로 인해 비정상적으로 종료될 경우 같은 프로세스에 소속된 다른 스레드까지 전부 종료된다.
또한 스레드를 강제 종료시킬 경우 해당 스레드와 같은 자원을 공유하던 다른 스레드에 영향을 줄 가능성이 있기 때문에 스레드에 대해서 교육할 때도 강제로 종료시키는 것은 자제하라고 교육하는경우가 많고 C/C++/Rust와 같은 언매니지드 언어[7]의 경우 강제 중단된 스레드가 메모리를 할당 받아서 사용하고 있던 경우 바로 메모리 누수로 이어지게 되므로 프로그래밍 언어 설계적인 면에서도 임의대로 종료시키지 못하게 하는 방향으로 가고 있다.[8]
5. CPU의 스레드
CPU의 공개된 스펙에서 4 코어 8 스레드 등으로 언급되는 스레드는 위에서 언급한 것과 같으나 단위로서의 뉘앙스가 더 강하다. 4개의 코어만으로 최대 8개의 작업을 동시에 처리할 수 있는 식이다.본래 CPU는 코어 하나당 단 하나의 작업만을 처리할 수 있지만 SMT[9]라는 기술을 사용하면 하나의 코어가 어느 정도의 다중 처리 능력을 가질 수 있다. 이럴 경우 물리적인 코어와 논리적인 코어(스레드)의 개수가 달라지게 된다. 물론 물리적인 코어라는 자원은 변하지 않기 때문에 물리적인 코어 2개가 있을 때보다 성능이 확실히 떨어진 편이다. 그렇지만 멀티스레딩에서는 많은 연산을 수행할 경우 상대적으로 다른 프로세스에 훨씬 많은 여유를 줄 수 있다는 장점이 있다.
사실 멀티코어 CPU 이전부터 존재하던 개념이었다. 문서를 작업하고 있는데 데스크탑 시계가 안 가거나 느리게 가면 큰일나기 때문이다.
6. 기타
리눅스 커널의 경우 2.2 버전까지는 프로세스로 스레드를 에뮬레이트하는 식으로 구현했었다. 이로 인해 멀티스레딩을 사용하면 가볍게 작동해야 할 프로그램이 오히러 더 무거워지는 기현상이 있었다. 다행히 2.4 버전부터 스레드를 제대로 구현하였기에 이러한 문제가 없다.[1] '쓰레드'라고도 부르는 사람도 있는데 정확한 명칭은 '스레드'다.[2] 프로세스마다 메모리가 관리되고 있기에 프로세스를 생성할 때 필요한 정보를 복사해줘야 하여 생성과 제거가 느리고 프로세스간 정보 교환이 어려운데다 상당량의 메모리를 중복해서 가지고 있어야 한다. 또한 프로세스가 많을수록 Context Switching 부담이 커진다.[3] CPU가 한 프로세스를 처리하던 중 다음 프로세스로 이동하면서 수행하는 동작, 각각 프로세스에는 자신이 사용하는 컴퓨터 자원(CPU 레지스터, 메모리 페이지 테이블 등)을 가지고 있는데 다음 프로세스로 전환할 때 현재 프로세스의 자원을 메인 메모리에 저장한 후 다음 프로세스의 자원을 메모리에서 불러오는 과정을 거친다. 이 과정에서 시간이 걸리기 때문에 자주 발생하면 성능이 저하된다.[4] pthread는 POSIX 기반 운영체제에서만 사용할 수 있다.[5] Process Control Block[6] 아예 없지는 않다. 서로 공유하는 파일을 깨먹는다거나 하면...[7] C#과 같은 .NET 계열 언어들과 Java, Kotlin과 같이 JVM 위에서 돌고 이런 언어들은 메모리를 유저 코드가 아닌 런타임이 관리하므로 강제 종료된 스레드가 할당한 자원이 GC가 동작하면서 자동으로 해제되지만 이러한 스레드가 상기한 언매니지드 언어로 작성된 라이브러리 코드를 불러와 쓰는 경우 까지는 해결해 주지 못한다.[8] 대표적으로 .NET 계열 언어에서는 닷넷 5.0부터는 쓰레드를 임의대로 종료시키는 Thread.Abort 메소드를 지원 중단하겠다고 선언했다.[9] 인텔의 하이퍼 스레딩이 이것의 일종.