[C++] 쓰레드 (thread)
- 프로세스 (Process)
프로세스란 운영체제에서 실행되는 프로그램의 최소 단위로서, 보통 하나의 프로그램을 가리킨다. 프로세스는 운영체제로부터 자원을 할당받아 실행되며, 프로그램의 코드와 데이터, 메모리 공간, 파일 디스크립터 등을 포함한다. 각 프로세스는 독립적인 실행 환경을 가지며, 서로 메모리를 공유하지 않는다.
프로세스가 실행되면 CPU의 코어에서 프로그램의 명령어를 실행하게 되는데, 이때 여러 프로세스가 동시에 실행되는 것처럼 보이지만, 실제로는 각 프로세스가 작은 단위로 번갈아가며 실행되는 것이다. 이렇게 프로세스 간에 스위칭되는 과정을 컨텍스트 스위칭이라고 하며, 이를 운영체제의 스케쥴러가 관리한다.
- 쓰레드 (Thread)
또한, 프로세스 내에서 실행되는 작은 실행 단위를 쓰레드라고 한다. 프로세스는 하나 이상의 쓰레드로 구성될 수 있으며, 같은 프로세스 내의 쓰레드들은 메모리를 공유하여 데이터를 주고받을 수 있다. 멀티쓰레드 프로그램은 여러 쓰레드가 동시에 작업을 수행함으로써 성능을 향상시킬 수 있다.
즉, 프로세스는 실행되는 프로그램의 최소 단위로서, 각각 독립적인 실행 환경을 가지며 운영체제의 자원을 할당받아 작업한다. 이 프로세스 내에서 쓰레드가 실행되며, 멀티쓰레드 프로그램은 여러 쓰레드가 동시에 작업하여 효율을 높인다.
요즘 CPU의 발전 방향은 코어 하나의 동작 속도를 높이는 것보다는 CPU에 장착된 코어 개수를 늘리는 방향으로 진화하고 있다. 예를 들어, 인텔의 i5 모델은 4개의 코어를 가지며, AMD의 라이젠 모델은 8개의 코어를 가진다. 이러한 멀티코어 프로세서에서는 여러 개의 코어가 동시에 작업을 수행하면서 성능을 향상시킨다.
이러한 발전으로 인해 멀티코어 프로세서에서는 병렬 가능한 작업들을 동시에 처리할 수 있다. 예를 들어 1부터 10000까지 더하는 작업을 생각해보면, 싱글 쓰레드로 처리할 때는 단순히 순차적으로 덧셈을 수행한다. 그러나 멀티코어 프로세서에서는 쓰레드를 분할하여 동시에 여러 쓰레드에서 작업을 수행하면서 성능을 크게 향상시킬 수 있다.
이를테면 8개의 코어를 가진 CPU에서는 8개의 쓰레드를 병렬로 수행할 수 있다. 각 쓰레드는 1부터 1250까지 덧셈을 수행하고, 모든 쓰레드가 작업을 완료하면 그 결과를 합쳐서 최종 결과를 얻을 수 있다. 이렇게 하면 작업이 훨씬 빠르게 처리되며, 싱글 쓰레드의 경우보다 약 8배의 성능 향상을 얻을 수 있다.
도색
요약하자면, 멀티코어 프로세서의 발전은 병렬 처리를 통해 작업을 동시에 처리하면서 성능을 향상시키는 방향으로 진화하고 있다. 이로 인해 병렬 가능한 작업들을 더 효율적으로 처리할 수 있게 되었으며, 멀티코어 프로세서의 성능은 싱글 쓰레드의 경우보다 훨씬 빠른 작업을 가능케 한다.
작업을 여러 개의 다른 쓰레드를 이용해서 빠르게 수행하는 것을 병렬화(parallelize)라고 한다. 그러나 모든 작업이 병렬화가 가능한 것은 아니다.
예를 들어, 가구를 만든는 공장을 생각해 본다.
- 목재 자르기
- 조립
- 연마 및 마감
- 도색
- 검수
가구를 만드는 공장에서 연마 및 마감 작업이 있다. 이 작업은 조립 작업이 완료된 가구를 대상으로 진행되어야 한다. 따라서 연마 및 마감 작업을 별도의 쓰레드에서 병렬화하려면, 조립 작업이 완료된 가구를 대기 없이 연마 및 마감 작업에 전달할 수 있는 구조가 필요하다.
그러나 조립 작업이 완료되기 전까지는 연마 및 마감 작업을 수행할 수 없으므로, 이런 종속성 때문에 연마 및 마감 작업을 병렬화하기가 어려울 수 있다. 즉, 프로그램의 논리 구조 상에서 의존 관계가 있는 작업들을 병렬화하려면 추가적인 처리와 조정이 필요하며, 때로는 이로 인해 오히려 성능이 저하될 수 있다. 결과적으로 병렬화로 얻는 이점이 없어지게 된다.
이처럼 프로그램의 논리 구조나 작업들 간의 의존성이 병렬화를 어렵게 만드는 경우가 있다. 병렬화는 작업들 간의 독립성과 데이터 공유에 대한 고려가 필요하며, 특히 복잡한 의존 관계를 가진 경우 효과적으로 병렬화하기 위해서는 더 신중한 계획과 설계가 필요하다.
즉, 의존 관계로 인해 프로그램의 논리 구조가 병렬화를 어렵게 만들 수 있다. 연산들 간의 의존성이 많을수록 병렬화가 어려워지며, 반대로 독립적으로 수행 가능한 구조일수록 병렬화가 쉬워진다.
C++에서 스레드(Thread)는 동시에 실행되는 작업의 단위를 나타내며, 멀티스레딩(Multithreading)을 통해 여러 작업을 동시에 처리할 수 있도록 해준다. C++11부터는 <thread> 헤더를 사용하여 스레드 관련 기능을 지원한다.
- 쓰레드 예제 코드 1
#include <iostream>
#include <thread>
// 스레드에서 실행될 함수
void threadFunction(int id) {
std::cout << "Thread " << id << " is running." << std::endl;
}
int main() {
// 스레드 생성 및 시작
std::thread thread1(threadFunction, 1);
std::thread thread2(threadFunction, 2);
// 스레드가 끝날 때까지 대기
thread1.join();
thread2.join();
std::cout << "All threads have finished." << std::endl;
return 0;
}
위 코드에서 std::thread 클래스를 사용하여 스레드를 생성하고 시작한다. 생성자에는 실행될 함수와 인자들을 전달한다. join() 메서드를 호출하여 스레드가 끝날 때까지 기다린 후, 모든 스레드가 끝났을 때 메인 스레드가 "All threads have finished." 메시지를 출력한다.
- 쓰레드 예제 코드 2
#include <iostream>
#include <thread>
#include <chrono>
void threadFunction(int id) {
std::this_thread::sleep_for(std::chrono::seconds(id)); // 스레드가 id만큼 대기
std::cout << "Thread " << id << " is running." << std::endl;
}
int main() {
std::thread thread1(threadFunction, 1);
std::thread thread2(threadFunction, 2);
// get_id()를 사용하여 스레드의 고유 ID를 얻어와 출력
std::cout << "Thread 1 ID: " << thread1.get_id() << std::endl;
std::cout << "Thread 2 ID: " << thread2.get_id() << std::endl;
// 스레드 1은 join()으로 대기하며 스레드 2는 detach()로 분리
thread1.join();
thread2.detach();
// 스레드 2가 독립적으로 실행되도록 시간을 주기 위해 대기
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "Main thread finishes." << std::endl;
return 0;
}
스레드의 메서드에는 join(), detach(), get_id(), joinable() 등이 있다.
- join()은 스레드가 끝날 때까지 대기하는 역할을 한다.
join():
더보기join()은 호출한 스레드가 대상 스레드의 종료를 기다리도록 만든다.
대상 스레드가 종료되기 전까지 호출한 스레드는 블록되어 대기하며, 대상 스레드가 종료되면 호출한 스레드가 실행을 계속한다.
join()을 호출한 스레드는 대상 스레드의 실행이 끝나기를 기다리므로 스레드 간의 순서를 조절하거나, 스레드의 결과를 수집하는 데 사용할 수 있다. - detach()는 스레드를 떼어놓고 독립적으로 실행하도록 한다.
더보기detach()는 호출한 스레드와 대상 스레드를 분리한다.
분리된 스레드는 호출한 스레드와 별개로 실행되며, 호출한 스레드가 종료해도 대상 스레드는 계속 실행된다.
주로 백그라운드에서 동작하거나 더 이상 해당 스레드를 제어하지 않을 때 사용된다.
분리된 스레드는 자신이 종료될 때 자동으로 리소스가 정리되지만, 분리 후에는 해당 스레드를 제어할 수 없다. - get_id()는 스레드의 고유 ID를 얻어오는 역할을 한다.
get_id():
더보기get_id() 메서드는 스레드의 고유한 ID를 반환하는데, 스레드가 생성될 때마다 시스템에서 할당되는 고유한 값입니다. 이 ID를 사용하면 어떤 스레드가 어떤 작업을 수행하는지를 구분할 수 있다.
get_id()를 사용하는 이유와 장점은 다음과 같다.
1. 쓰레드 식별: 여러 개의 스레드가 동시에 실행되는 경우, 각 스레드가 어떤 작업을 수행하고 있는지를 구분하기 위해 사용된다. 디버깅이나 모니터링 시 유용하다.
2. 동기화: 특정 스레드를 기다리거나, 분리시키거나 할 때 스레드의 ID를 사용하여 식별하고 제어할 수 있다.
3. 쓰레드 관리: 스레드가 종료될 때까지 대기할 때 join()을 사용하는데, 어떤 스레드를 기다릴지를 스레드의 ID를 통해 선택할 수 있다.
get_id()를 통해 스레드를 식별하면 스레드 간의 상호작용이나 제어가 더욱 효율적으로 이루어질 수 있다.
4. joinable()는 해당 스레드가 아직 종료되지 않았고, join() 또는 detach()를 호출할 수 있는지 여부를 확인하기 위해 사용된다.
joinable() 메서드의 반환값은 bool 형식으로, true라면 스레드가 아직 실행 중이며 join() 또는 detach()를 호출할 수 있다는 것을 의미한다. 반대로 false라면 스레드가 종료되었거나 이미 분리된 상태라는 것을 나타낸다.
#include <iostream>
#include <thread>
void threadFunction() {
std::cout << "Thread is running." << std::endl;
}
int main() {
std::thread myThread(threadFunction);
if (myThread.joinable()) {
std::cout << "Thread is joinable." << std::endl;
} else {
std::cout << "Thread is not joinable." << std::endl;
}
myThread.join(); // 스레드 실행을 기다림
if (!myThread.joinable()) {
std::cout << "Thread has finished." << std::endl;
}
return 0;
}
myThread 스레드를 생성하고, joinable() 메서드를 사용하여 스레드의 상태를 확인하고 출력한다. 그 후 join()을 호출하여 스레드의 실행이 끝날 때까지 기다리고, 다시 joinable()을 호출하여 스레드가 종료된 것을 확인한다. joinable() 메서드는 스레드의 상태를 확인하고 스레드를 안전하게 조작하기 위해 유용하게 사용된다.
스레드는 동시에 실행되기 때문에 스레드 간 동기화가 중요한 문제이다. 동시에 공유된 데이터에 접근할 때는 뮤텍스(Mutex)나 락(Lock)과 같은 동기화 메커니즘을 사용하여 스레드 간의 충돌을 방지해야 한다.