프로세스와 스레드
프로세스: 시스템 리소스 할당의 최소 단위, 일반적으로 실행 중인 프로그램 인스턴스로 정의됨 스레드: 시스템 작업 스케줄링의 최소 단위
프로세스 간 통신: 파이프, 세마포어, 신호, 메시지 큐, 공유 메모리, 소켓 스레드 간 통신: 락 메커니즘, 세마포어 메커니즘, 신호 메커니즘, 배리어
동기화: 작업 조각의 순서를 보장하는 것 상호 배제: 동일한 시점에 하나의 스레드만 자원을 사용하도록 하여 데이터 일관성을 유지하는 것
동기화 방법
- 원자적 연산
- 세마포어(semaphore)
- 읽기/쓰기 세마포어(rw_semaphore)
- 상호 배제 락(mutex lock)
- 스핀 락(spinlock)
- 읽기/쓰기 락(rwlock)
- 순차 락(seqlock)
순차적 자원 할당 방법 은행가 알고리즘, 데드락 생산자/소비자 문제 읽기/쓰기 문제 철학자의 저녁식사 문제, 상호 배제
C++ 멀티스레딩 프로그래밍에서 사용되는 헤더 파일은 다음과 같습니다.
<thread>
<mutex>
<condition_variable>
<future>
<promise>
<packaged_task>
<atomic>
<async>
데드락
데드락 발생의 네 가지 필요 조건
| 조건 | 설명 |
|---|---|
| 상호 배제 | 리소스는 동시에 한 프로세스만 사용 가능 |
| 요청 및 유지 | 프로세스가 리소스를 요청할 때 이미 점유하고 있는 리소스를 해제하지 않음 |
| 비강탈 | 프로세스가 얻은 리소스는 사용 완료 전까지 강제로 해제되지 않음 |
| 순환 대기 | 프로세스들 사이에 순환적인 리소스 대기 관계 형성 |
데드락 예방
데드락 발생 조건들을 깨트리는 방법
- 상호 배제 조건 파괴
- 비강탈 조건 파괴
- 요청 및 유지 조건 파괴
- 순환 대기 조건 파괴
데드락 회피
리소스 할당의 안전성을 검사하여 순환 대기를 방지하는 방법, 예: 은행가 알고리즘
데드락 감지
데드락 발생을 허용하되 감지 방법을 제공
데드락 해소
발생한 데드락을 해결하기 위해 리소스 강제 해제 또는 스레드 취소
C++11 스레드 라이브러리
C++11에서는 표준 스레드 라이브러리 <thread>를 도입하였으며, std 네임스페이스에 위치하며 컴파일 시 -std=c++11 옵션을 추가합니다.
생성자
- 기본 생성자
thread() noexcept;
joinable하지 않은 빈 thread 객체를 생성합니다.
- 초기화 생성자
template<typename Fn, typename... Args>
explicit thread(Fn&& fn, Args&&... args);
joinable한 thread 객체를 생성합니다.
새로운 스레드는 fn()을 호출하며, 인수는 args로부터 전달됩니다.
복사 생성자
thread(const thread&) = delete;
복사는 금지되어 있으며, thread 객체는 복사할 수 없습니다.
이동 생성자
thread(thread&& x) noexcept;
임시(익명) thread 객체를 다른 thread 객체로 이동할 수 있게 합니다. 성공 후, x는 더 이상 어떤 thread 객체도 나타내지 않습니다.
주의사항: 각 thread 객체는 소멸되기 전에 반드시 join()을 호출하여 주 스레드가 자식 스레드가 완료될 때까지 기다리거나 detach()를 호출하여 두 스레드를 분리해야 합니다. 그렇지 않으면 다음 문제가 발생할 수 있습니다:
- 스레드가 차지한 리소스가 모두 해제되지 않아 메모리 누수가 발생합니다.
- 주 스레드가 완료되었으나 자식 스레드가 아직 완료되지 않았을 경우 프로그램 실행이 예외를 발생시킵니다.
예제 코드는 다음과 같습니다:
#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
#include <functional>
#include <atomic>
void 함수1(int 번호) {
for (int i = 0; i < 5; ++i) {
std::cout << "스레드 " << 번호 << " 실행 중\n";
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void 함수2(int& 카운터) {
for (int i = 0; i < 5; ++i) {
std::cout << "스레드 2 실행 중\n";
++카운터;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
int 카운터 = 0;
std::thread t1; // t1은 스레드가 아닙니다.
std::thread t2(함수1, 카운터 + 1); // 값으로 전달
std::thread t3(함수2, std::ref(카운터)); // 참조로 전달
std::thread t4(std::move(t3)); // t4는 이제 함수2()를 실행 중입니다. t3은 더 이상 스레드가 아닙니다.
t2.join();
t4.join();
std::cout << "카운터의 마지막 값은 " << 카운터 << '\n';
}
대입 연산자
- 복사 대입
thread& operator=(const thread&) = delete;
복사는 금지되어 있으며, thread 객체는 복사할 수 없습니다.
- 이동 대입
thread& operator=(thread&& rhs) noexcept;
대입 대상 객체가 joinable하지 않은 경우 오른쪽 값을 가진 임시 객체 rhs를 전달해야 합니다. 그렇지 않으면 terminate() 에러가 발생합니다.
예제 코드는 다음과 같습니다:
#include <iostream>
#include <chrono>
#include <thread>
void 스레드_작업(int 초) {
std::this_thread::sleep_for(std::chrono::seconds(초));
std::cout << "안녕하세요, 스레드 "
<< std::this_thread::get_id()
<< ", " << 초 << "초간 대기했습니다.\n";
}
int main() {
std::thread 스레드들[5];
std::cout << "5개의 스레드 생성...\n";
for (int i = 0; i < 5; i++) {
스레드들[i] = std::thread(스레드_작업, i + 1);
}
std::cout << "스레드 생성 완료! 이제 합류를 기다립니다\n";
for (auto& t : 스레드들) {
t.join();
}
std::cout << "모든 스레드 합류 완료.\n";
return 0;
}
다른 멤버 함수들
| 함수 | 설명 |
|---|---|
joinable() |
스레드 객체가 join 함수를 호출할 수 있는지 여부를 반환 |
join() |
현재 스레드를 블록하여 스레드 객체가 실행 완료될 때까지 기다림 |
detach() |
스레드 객체를 해당 스레드에서 분리하여 독립적으로 실행되도록 함 |
swap() |
두 스레드 객체가 표현하는 기본 핸들을 교환 |
native_handle() |
std::thread 구현에 따라 특정 스레드 핸들을 반환 |
hardware_concurrency()[static] |
하드웨어 동시성 특성을 탐지하여 지원하는 스레드 동시성 수를 반환, 단 반환값은 시스템 힌트(hint)로서 사용 |
주의사항
- 만약 스레드가 작업을 완료했지만
join되지 않았다면, 해당 스레드는 여전히 활성화된 것으로 간주되며, 따라서join할 수 있습니다. detach()를 호출한 후에는
*this는 더 이상 스레드 인스턴스를 나타내지 않습니다.joinable() == falsestd::this_thread::get_id() == std::thread::id()
std::this_thread 네임스페이스 내의 관련 유틸리티 함수
<thread> 헤더 파일은 thread 클래스뿐만 아니라 this_thread라는 이름의 네임스페이스도 정의하며, 여기에는 유용한 함수들이 포함되어 있습니다.
| 함수 | 설명 |
|---|---|
get_id() |
현재 스레드 객체 ID 반환 |
yield() |
현재 스레드를 블록하여 운영 체제가 다른 스레드를 실행하도록 함 |
sleep_until() |
지정된 시간까지 현재 스레드를 블록 |
sleep_for() |
지정된 시간 동안 현재 스레드를 대기, 스레드 스케줄링 등의 이유로 실제 대기 시간은 sleep_duration보다 더 길 수 있음 |
예제 코드는 다음과 같습니다:
#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
#include <functional>
#include <atomic>
void 함수1(int 번호) {
for (int i = 0; i < 5; ++i) {
std::cout << "스레드 " << 번호 << " 실행 중\n";
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void 함수2(int& 카운터) {
for (int i = 0; i < 5; ++i) {
std::cout << "스레드 2 실행 중\n";
++카운터;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
int 카운터 = 0;
std::thread t1; // t1은 스레드가 아닙니다.
std::thread t2(함수1, 카운터 + 1); // 값으로 전달
std::thread t3(함수2, std::ref(카운터)); // 참조로 전달
std::thread t4(std::move(t3)); // t4는 이제 함수2()를 실행 중입니다. t3은 더 이상 스레드가 아닙니다.
t2.join();
t4.join();
std::cout << "카운터의 마지막 값은 " << 카운터 << '\n';
}