- 메모리에 올라와 실행되고 있는 프로그램의 인스턴스(독립적인 개체) - 운영체제로부터 시스템 자원을 할당받는 작업의 단위
스레드(Thread)란
- 프로세스 내에서 실행되는 여러 흐름의 단위
3. Thread -> 쓰레드 로드
(1) 쓰레드란 뭔가요?
- 스택은 실행메모리를 담고 실행 흐름(함수)이라고 한다.
-> 이 실행 흐름을 쓰레드라고 합니다.
(2) 쓰레드의 개수는 몇개가 적당하다고 생각한가요? -> 경험
- 암달의 법칙에 의거한CPU 코어 개수에 비례CPU 코어의 개수 *2 +1,CPU 코어의 개수 * 2 가 적당하다고 생각합니다.
- 다이렉트에서는 코어의 개수만큼 만듬
(3)10개의 일을 1000개의 쓰레드가 처리하면 더 빠를까요?
1개의 일에 대한 일의 처리량과 일 분배에 따라 다를거 같다. 예를들어 간단한 작업이 10개면 1000개의 쓰레드를 미리 만들어두면 일하는 쓰레드 따로 있고 아무것도 하지 않는 쓰레드도 존재할 것이기에 쓰레드를 만드는 비용까지 생각하면 낭비가 심하고 더 느리다고 생각합니다.
(4) 쓰레드를 쓰는 것만으로도 빨라질가요? 아니다. 대표적인 예시 (단일 쓰레드)
단일 쓰레드나 처리하는 일의 양에 비해 적은 수의 쓰레드 면더 느리다이유?
메인 쓰레드로만 처리하면 본래 일의 크기 작업 시간이 걸리지만 , 적은수 즉 단일 쓰레드로 하면, 쓰레드 만드는 비용, 안전하게 일 주고 받기등 의 처리들이 더 추가 되기에 본래 실행하는 시간보다 더 느리다.
(5) 사용할 쓰레드를 미리 만들어두고 사용하는 이유 (쓰레드 풀)? -> 메모리 풀?(검색해보기)
런타임 맨앞에 미리 쓰레드를 많이 만들고 다시는 안만든다는 것 (init)
한번 만든 쓰레드를 재활용해서 사용하겠다는 것입니다.
(6) 쓰레드가 공유하는 메모리 범위
쓰레드는 스택은 각자 메모리를 가지지만힙, 데이터, 코드는 공유한다.
-> 크리티컬 섹션이라고 부르는 임계영역은 각 쓰레드가 공유하는 메모리에서 발생한다.
(7) 쓰레드를 많이 만드면 좋은가 >?
- 많이 만드면 (비지 쓰레드) -> 양이 많아 지면 많아질수록 CPU 점유율 증가 -> 윈도우가 멈출 가능성
쓰레드 = 함수 비지 쓰레드 = 함수안의 반복문
8) 임계영역 , 크리티컬 섹션 -> 문제 어케 해결함?
- std::atomic
-> 아토믹은 언제 사용하느냐16바이트 이하의 아주 작은 메모리 영역의 쓰레드 동시접근을
빠르게 막고 싶을때 다른 쓰레드에 안전하게 접근할수 있게 해주는 기능들을 보통 락이라고 하는데
락중 빠른 계열에 속합니다.
- > Window:인터락
- > C++ std :아토믹=>내부에서window인터락을 쓰고 있다
-> 아토믹을 사용하니 잘 중단점이 걸리는 것을 알수 있고쓰레드에서안전하게 메모리를 보호하는 기능들중 가장 빠르지만
->단점이 하나 있는데 보호할수 있는 메모리의 크기가 16바이트->맵같이 크고 복잡한 자료구조는 보호가 불가능합니다.
- std::mutex
-> 그래서자료구조나 특정 영역을 보호할때는 mutex를 지원해준다.
-> std:;lock_guard와 std::lock의 차이점은?
std::lock_guard와std::lock은 모두C++에서 다중 스레드 환경에서의 동시성 문제를 해결하기 위한 기술 중 하나인 뮤텍스(mutex)와 관련된 클래스와 함수이다.하지만 각각의 역할 및 사용 방법이 다르기 때문에 차이가 있다.
(9)컨텍스트 스위칭과 CPU 연관성
- 쓰레드가 일을 하기 위해서 뭔가 준비를 하는 작업을 컨텍스트 스위칭이라고 합니다
- CPU에 실행할 프로세스를 교체
- 컨텍스트 스위칭이 일어나지 않은 쓰레드를 busy쓰레드라고 합니다.
컨텍스트 스위칭이 너무 자주 일어나도 일어나지 않아도 둘다 문제
컨텍스트 스위칭이 자주 일어나면 적은 작업량에 준비하는 작업들만 많이 하는 것으로 오히려 속도면에서 더 느릴 수 있다.
컨텍스트 스위칭이 잘 일어나지 않으면 그만큼 CPU를 계속 점유하고있기에(비지 쓰레드 처럼 동작) 이런 작업들이 많으면 많을수록 중간에 정지하는 것처럼 움직인다.
모든 적당한 일 분배, 적당한 작업량 등 -> 판단은 경험
-> 일분배 (IOCP) -> IOCP 공부하기 -> 아직 안끝남
적당한 수의 쓰레드를 이용해 일을 균등하게 분배해서 일을 시키는 것이 제일 베스트
쓰레드 추가 공부
!참고
: 쓰레드의 리턴값은 없다쓰레드는 리턴값이 없기 때문에 어떠한 결과를 반환하고 싶다면 포인터의 형태로 전달하면 된다.
JOIN
(1) 쓰레드는 언제 처리될지 모름
#include <string>
#include <vector>
#include <iostream>
#include <list>
#include <thread>
#include <functional>
void test1()
{
for (size_t i = 0; i < 10; i++)
{
std::cout << "쓰레드 1 실행중" << "\n";
}
}
void test2()
{
for (size_t i = 0; i < 10; i++)
{
std::cout << "쓰레드 2 실행중" << "\n";
}
}
void test3()
{
for (size_t i = 0; i < 10; i++)
{
std::cout << "쓰레드 3 실행중" << "\n";
}
}
int main()
{
std::thread th1,th2,th3;
// 서로 언제 처리될지 모름
th1 = std::thread(std::bind(test1));
th2 = std::thread(std::bind(test2));
th3 = std::thread(std::bind(test3));
// join 해당하는 쓰레드들이 실행을 종료하면 리턴하는 함수
th1.join();
th2.join();
th3.join();
}
-> join을 안해주면 터짐
이유?
쓰레드들의 내용이 실행되기 전에 main 함수가 종료되었기 때문이다.
-> 앞서 말한 쓰레드는 언제 실행되고 언제 종료 될지 모르기 때문에 확실한 작업 종료의 리턴을 확인을 못하고 했을 때 main 함수가 먼저 종료되어버려 터짐
C++ 표준에 따르면, join 되거나 detach 되지 않은 쓰레드들의 소멸자가 호출된다면 예외를 발생시키도록 명시되어 있다. 따라서, 쓰레드 객체들이 join이나 detach모두 되지 않았으므로 위와 같은 문제가 발생하게 된다.
detach
detach란 말 그대로, 해당 쓰레드들을 실행시킨 후, 잊어버리는 것이라 생각하면 된다. 대신 쓰레드는 백그라운드에서 돌아가게 된다.
#include <string>
#include <vector>
#include <iostream>
#include <list>
#include <thread>
#include <functional>
void test1()
{
for (size_t i = 0; i < 10; i++)
{
std::cout << "쓰레드 1 실행중" << "\n";
}
}
void test2()
{
for (size_t i = 0; i < 10; i++)
{
std::cout << "쓰레드 2 실행중" << "\n";
}
}
void test3()
{
for (size_t i = 0; i < 10; i++)
{
std::cout << "쓰레드 3 실행중" << "\n";
}
}
int main()
{
std::thread th1,th2,th3;
// 서로 언제 처리될지 모름
th1 = std::thread(std::bind(test1));
th2 = std::thread(std::bind(test2));
th3 = std::thread(std::bind(test3));
// detach -> 해당 쓰레드들을 실행시킨 후, 잊어버리는 것
th1.detach();
th2.detach();
th3.detach();
std::cout << "main 함수 종료" << "\n";
}
기본적으로 프로세스가 종료될 때, 해당 프로세스 안에 있는 모든 쓰레드들은 종료 여부와 상관없이 자동으로 종료된다. 즉 main 함수에서 메인 함수 종료!를 출력하고, 프로세스가 종료하게 되면, func1, func2, func3 모두 더 이상 쓰레드 작동중!을 출력할 수 없게 된다. 쓰레드들을 detach 했기 때문에 main함수에서 다른 쓰레드들이 종료될 때까지 기다리지 않은 모습을 확인할 수 있다. 위 부분이 그냥 쭈르륵 실행되어서 쓰레드들이 채 문자열을 표시하기 전에 프로세스가 종료된 것이다.
std::thread::joinable()
쓰레드가 실행 중인 활성 쓰레드인지 아닌지 확인합니다.
std::atomic -> 리소스 로딩 할때 사용 -> 참고 해서 공부해보기
->
int NormalN = 100;
(1) 작업이 겹치는 순간이 발생 -> 임계영역, 크리티컬 섹션 (공유하는 메모리에 작업 겹침)
std::atomic_int atomicN = 100;
-> std::atomic 겹치는 메모리 공간 작업 통제
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
#include <functional>
// 전역 변수 -> 데이터 영역
int NormalN = 100;
std::atomic_int atomicN = 100;
void NormalAdd(const int n)
{
for (size_t i = 0; i < 100; i++)
{
--NormalN;
std::cout <<n<<" 번 쓰레드 " << NormalN << "\n";
if (NormalN == 0)
{
break;
}
}
}
void NormalAtomic(const int n)
{
for (size_t i = 0; i < 100; i++)
{
--atomicN;
std::cout << n << " 번 쓰레드 " << atomicN << "\n";
if (atomicN == 0)
{
break;
}
}
}
int main()
{
std::thread th1, th2,th3;
/* th1 = std::thread(NormalAdd,1);
th2 = std::thread(NormalAdd,2);
th3 = std::thread(NormalAdd,3);
if (th1.joinable())
{
th1.join();
}
if (th2.joinable())
{
th2.join();
}
if (th3.joinable())
{
th3.join();
}*/
th1 = std::thread(NormalAtomic, 1);
th2 = std::thread(NormalAtomic, 2);
th3 = std::thread(NormalAtomic, 3);
if (th1.joinable())
{
th1.join();
}
if (th2.joinable())
{
th2.join();
}
if (th3.joinable())
{
th3.join();
}
}
std::mutex -> 쓰레드를 통해서 안전하게 자료구조 관리
(1) mutex 보호 없이 사용하는 경우
// 전역 변수 -> 데이터 영역
std::vector<int> num;
//std::mutex num_mutex;
std::function<void()> test;
test = [] {
for (int i = 0; i < 10; i++)
{
//std::lock_guard<std::mutex> Lock(num_mutex);
num.push_back(i);
}
};
th1 = std::thread(test);
th2 = std::thread(test);
th3 = std::thread(test);
- 터지거나 잘 실행되는 경우도 있음 -> 언제 어떻게 터질지 모르기에 std::mutex 해주는게 맞음
std::lock_guard는 많이 쓰이는 락 종류로써 다음처럼 객체 생성 시에 lock되며 객체가 소멸시에 unlock 되는 특성을 가지고 있습니다.
-> 알아서 Lock 해주며 Unlock 해줌 사용하기 편함
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <mutex>
#include <windows.h> // Sleep();
// 전역 변수 -> 데이터 영역
std::vector<int> num;
std::mutex num_mutex;
int main()
{
std::thread th1, th2,th3;
std::function<void()> test;
test = [] {
for (int i = 0; i < 10; i++)
{
// 공통된 자료구조에 값을 넣어 줄때 멀티 쓰레드면 동시에 접근하는 경우도 있다.
// 그래서 동시 접근시 들어온 순서대로 넣어주기 위해
// lock과 unlock 개념 이용
// 누군가 들어오면 뒤에 들어오는 데이터는 잠시 대기
// 다 들어가고 나서 다음 순번 처리
// -> mutex 사용해서 자료구조 관리할거면 필수 !
std::lock_guard<std::mutex> Lock(num_mutex);
num.push_back(i);
}
};
th1 = std::thread(test);
th2 = std::thread(test);
th3 = std::thread(test);
th1.join();
th2.join();
th3.join();
for (int i = 0; i < num.size(); i++)
{
std::cout << num[i] << "\n";
Sleep(1000); //1000ms로 1초를 의미합니다. -> Delay 생각
}
}
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <mutex>
#include <windows.h> // Sleep();
// 전역 변수 -> 데이터 영역
std::vector<int> num;
std::mutex num_mutex;
int main()
{
std::thread th1, th2, th3;
std::function<void()> test;
std::function<void()> SleepFun;
test = [] {
for (int i = 0; i < 10; i++)
{
// 공통된 자료구조에 값을 넣어 줄때 멀티 쓰레드면 동시에 접근하는 경우도 있다.
// 그래서 동시 접근시 들어온 순서대로 넣어주기 위해
// lock과 unlock 개념 이용
// 누군가 들어오면 뒤에 들어오는 데이터는 잠시 대기
// 다 들어가고 나서 다음 순번 처리
// -> mutex 사용해서 자료구조 관리할거면 필수 !
std::lock_guard<std::mutex> Lock(num_mutex);
num.push_back(i);
}
};
SleepFun = [] {
Sleep(5000);
};
th1 = std::thread(test);
th3 = std::thread(SleepFun);
th2 = std::thread(test);
if (th1.joinable())
{
th1.join();
}
if (th2.joinable())
{
th2.join();
}
if (th3.joinable())
{
th3.join();
}
for (int i = 0; i < num.size(); i++)
{
std::cout << i << "\n";
//Sleep(1000); //1000ms로 1초를 의미합니다. -> Delay 생각
}
}
쓰레드는 언제 작업이 끝날지 모르기때문에 주의 해야한다. ex) 렌더링 로드 중인데 관련한 Actor를 스폰할 수 있나?
-> DX 쓰레드 로드 확인하고 공부하기
- > 추가적 지식
(1) DX기준 쓰레드 동작하는 것
-라이브러리추가 한 것들 (FMOD, Imgui, 폰트등)
-Ntdll 쓰레드를비주얼스튜디오가 내 프로그램을 감시하는 쓰레드이다. ->비주얼 스튜디오가 감시하기 위한 쓰레드
- 주 쓰레드
(2) DX 엔진에서 사용하는 쓰레드 종류
-Std::thread는 내부에서 결국os가 제공해주는 쓰레드 함수를 사용해서쓰레드를 만드는 것이다
- Std::thread NewThread라고 쓰지만Window에서 사용한다면 내부에서__beginthreadex나__beginthread를사용하고 있다
(3) #include <window>
-Window에서 원자성을 지켜주는 기능은 인터락 함수들이라고 부른다.
- Std::가thread를 초반에std로 제공못해줬던 이유다. ->그냥 변수를 쓰는것보다 무조건 더 느리고 무조건더 연산이 많아질수밖에 없습니다.
-쓰레드를 여러 개 쓰면 내부에서 임계영역을 막기 위해서 여러가지 함수나 기능들을 사용해야 한다.그것때문에 쓰레드로 로딩하는게더 느려질수도 있다.
Ex)
그냥 로딩1초=>쓰레드에 안전하지 않으므로 게임자체가 망할수가 있다.
쓰레드에 안전한 로딩3초
속력
- 단일 쓰레드는 보통 더 느릴것이다.특히나 단일쓰레드 로딩은 무조건적인 느리게 만드는 것이다.
- 쓰레드는 일반적으로 순서 상관없이 최대파워로 최대한 일을 나눠서 다수의 쓰레드로 처리해야진정한 속력의 증가를 볼수가 있다는 것이다.