쓰레드 강의 다시한번 쭉 읽었음.
대부분 내용은 개념적인것만 이해했고 구현은 하나도 못하겠지만 나중에 thread쪽만 깊이 파서 다시 공부를 해봐야 할 내용인듯
/*******************************************************************************************/
하나의 cpu 코어로 여러개의 프로세스를 동시에 가동하는 것 처럼 보이는 것은
운영체제의 스케쥴러에 따라서
컨텍스트 스위칭 되가면서 번갈아 실행되고 잇는 것
쓰레드란?
cpu코어에서 돌아가는 프로그램 단위를 쓰레드라고 부름
프로세스가 아니엇나봐?
cpu 코어 한개는 한번에 하나의 쓰레드를 실행시킴
한개의 프로세스는 최소 한개의 쓰레드로 이루어져 잇음
여러개의 쓰레드로 구성된 프로그램을 멀티쓰레드 프로그램이라고 함
쓰레드와 프로세스의 차이
프로세스들은 메모리를 공유하지 않음
프로세스1은 프로세스2의 메모리에 접근 불가능하고 프로세스2는 프로세스1의 메모리에 접근 불가능
하지만 한 프로세스 안에 잇는 여러 쓰레드는 같은 메모리를 공유함
쓰레드1과 쓰레드2가 같은 변수에 접근 할수 이승ㅁ
현대 기술의 발전으로 cpu는 코어가 한개가 아니고 여러개임
그러므로 멀티코어 cpu에서는 각 코어에서 각기 다른 쓰레드들이 동시에 실행되고 잇음
코어가 늘어나면서 멀티쓰레드로 프로그램을 만드는게 유리해짐
멀티쓰레드로 만들기 유리한 프로그램은?
- 병렬 가능한 작업들
1~10000까지 더하는 작업을 단일 쓰레드 프로그램으로 짠다면 그냥 for문으로 1~10000까지 더하면 됨
하지만 이걸 10개의 쓰레드로 만든다면
1번쓰레드는 1~1000까지, 2번 쓰레드는 1001~2000까지... 하고나서 다 합치면
실행시간이 거의 10분의 1 수준이 될 수 잇음
하지만 모든 프로그램을 멀티쓰레드로 짠다고 유리한게 아님
피보나치 수열의 경우 n번째를 구하기 위해선 n-1과 n-2를 알아야함. 그리고 n+1을 구하기 위해선 n과 n-1을 알아야하고
그러니 다음 연산을 하려면 이전 연산의 결과가 필요한 이런 경우 병렬화가 매우 어려움
- 대기시간이 긴 작업들
인터넷에 잇는 자료를 스크랩 한다면 인터넷 속도가 cpu속도보다 느리므로 응답을 기다리기만 하는 시간이 생김
이런 경우 여러개의 쓰레드로 요청하고 응답이 오기까지 또 다른 쓰레드가 요청하고 또 다른 쓰레드가 요청하고
이러다가 응답이 오면 다운받고, 또 다운받고 하면 대기시간에도 다른 일으 할 수 이승ㅁ
쓰레드 생성법
C++ 11 이전에는 표준이 없어서 리눅스는 pthread_create로, 윈도우는 CreateThread로 만드는 등 복잡햇지만
C++ 11 이후로는 표준이 나와서 쉽게 사용 가능
#include <thread> // 헤더파일
thread t1(func1); // 객체 생성
// t1객체는 인자로 받은 func1을 새로운 쓰레드에서 실행함
thread t2(func2); // 이런식으로 여러 쓰레드를 만들어서 동시 실행 가능
// 단 스케쥴러 마음대로 조정되는거라 완전 동시는 아님
thread t3(func3, 1, 10, &res);
// 위처럼 func3 함수의 매개변수로 1, 10, &res를 주고싶으면 뒤에 쭉 써주면 됨
쓰레드 사용법
t1.join(); // join은 해당 쓰레드가 종료되면 리턴하는 함수
// 즉 t1.join()은 t1이 종료될때까지 리턴하지 않음
// 쓰는 이유는 쓰레드가 종료되기도 전에 메인함수가 종료되서 쓰레드 객체드링 소멸하는것을 막기 위해
// join은 아예 해당 쓰레드가 종료될때 까지 뒷쪽 코드는 실행하지 않게 sleep하는것과 비슷한 듯
t1.detach(); // detach는 쓰레드 실행시킨 후 잊어버린다라는 개념. 메인함수가 종료되어도 백그라운드에서 쓰레드는 계속 작동함
// 그러니 join을 쓸 필요없이 detach를 쓰면 메인함수와 상관없이 t1의 일이 끝나면 쓰레드가 소멸함
detach()를 하면 출력문같은건 메인함수가 끝나면 출력이 더 안되지만 다른 로직은 다 백그라운드에서 돌아가는 듯
쓰레드가 실행중인 함수가 cout을 할때 주의점
기본적으로 cout << A; 를 하게 되면 A가 잘 출력됨
그런데 cout << A << B; 를 하게 되면 A가 다 출력 된 후 B가 출력되어야 하는데
A출력완료, B출력시작 이 틈에 다른 쓰레드가 끼어들어서 AB순이 아닌 다른 쓰레드 출력 내용이 막 끼어들 수 있음
그러니 printf로 문장 format 하나로 해서 출력하면 이런걸 막아 줄 수 있음
서로 다른 쓰레드들이 같은 자원을 사용할때 발생하는 문제를 경쟁 상태(race condition)이라고 부름
경쟁 상태를 잘 조정해주기 위해 세마포어나 뮤텍스같은걸 사용
세마포어는 C++ 20버전에 추가라고 하고 mutex는 이미 있으니 mutex 쓰면 될 듯
C++에선 mutex 객체를 제공하는 듯 하니 뮤텍스 쓰면 될듯
void worker(int& result, std::mutex& m) {
for (int i = 0; i < 10000; i++) {
m.lock();
result += 1;
m.unlock();
}
}
위 형태처럼 뮤텍스 객체를 매개변수로 같이 넘겨주기만 하고
임계영역에 lock unlock으로 감싸주면 됨
다만 코드가 아주 길어지다보면 lock은 했는데 unlock을 빼먹는다던가, 틀린 위치에 적는 실수를 할 수 있음
그래서 소멸자에서 그냥 unlock을 하게 만들어 버리는 방법이 있는데
std::lock_guard<std::mutex> lock(m);
result += 1;
그래서 그냥 위처럼 적어주면 함수가 끝날때 lock이 소멸하면서 m을 알아서 unlock해줌
이 부분은 unique_ptr 보고 난 후에 봐야 정확히 이해 될듯
try_lock()
if (!m1.try_lock())
위처럼 현재 lock 할 수 있는지 확인해보고 lock 할 수 있다면 lock해주고 true리턴
lock할수 없다면 lock하지 않고 false리턴
데드락을 피하기 위한 가이드라인
1) ★쓰레드 한개에는 가급적 1개의 Lock만 쓴다
=> 모든 쓰레드들이 최대 한개의 Lock만 소유하면 데드락이 생기지 않음
대부분 경우 1개의 lock으로 되니까 여러개의 lock을 써야할 것 같으면 정말 꼭 써야할지 되짚어보기
2) mutex를 정해진 순서대로 써야 함
=> func1 에서는 m1, m2 순으로 락을 하는데, func2에선 m2, m1 순으로 락을 하면 데드락이 발생함
근데 애초에 뮤텍스를 한개만 쓴다는 1번 원칙을 따르면 이또한 문제될 일이 없음
생산자-소비자 패턴
생산자란 처리할 일을 받아오는 쓰레드
소비자란 받은 일을 처리하는 쓰레드
예를들어 인터넷에서 페이지를 긁어서 분석하는 프로그램을 만든다면
페이지를 긁어오는 쓰레드가 생산자, 분석하는 쓰레드가 소비자
생산자가 인터넷에서 페이지를 다운받아오는 속도는 다소 느린 반면
소비자가 분석하는 속도는 빠르다면
소비자가 할 일이 있는지 계속 확인하게 될 텐데 이게 비효율적이므로
condition_variable을 활용해서 생산자가 일이 들어왔을때만 소비자에게 알리는 식으로 처리 가능
/*******************************************************************************************/
아래 내용은 병렬처리에 대한 내용인데 상당히 복잡해서 일단 개념만 이해해두자...
쓰레드풀
- 쓰레드들을 위한 직업 소개소
- 여러 쓰레드들이 대기하고 있다가 할일이 들어오면 대기하고 있던 쓰레드들 중 하나가 그 일을 실행
- 예를들어 서버는 클라이언트 요청이 들어오면 해당 요청을 쓰레드풀에 추가만 하면 됨
그럼 쓰레드들 중 하나가 처리를 함
파이프라이닝
한 작업이 끝나기 전에 다른 작업을 시작하는 방식으로 동시에 작업하는 방식
예를들어 빨래를 할때
세탁기돌리기 -> 건조기 돌리기 -> 빨래 개기 -> 세탁기돌리기 -> 건조기 돌리기 -> 빨래 개기 -> ...
처럼 선행적으로 할수도 있지만 이러면 세탁기 돌아가는 동안은 건조기는 쉬게되고 빨래 개는것도 쉬게됨
효율적으로 하려면
세탁기돌리기 -> 건조기 돌리기 -> 빨래 개기 -> 세탁기돌리기 -> 건조기 돌리기 -> 빨래 개기 -> ...
-> 세탁기돌리기 -> 건조기 돌리기 -> 빨래 개기 -> 세탁기돌리기 -> 건조기 돌리기 -> ...
-> 세탁기돌리기 -> 건조기 돌리기 -> 빨래 개기 -> 세탁기돌리기 -> ...
위처럼 하면 한번에 3가지 작업을 다 할 수 있으니 더 빨라짐
cpu는 fetch -> decode -> excute -> write 4가지 작업을 해야하기 때문에
파이프라이닝을 적용해야 효율적임
근데 만약에 건조기 돌리는 시간은 1시간 걸리는데, 빨래 개는시간은 5분만에 된다면
실제로 3가지 사이클을 동시에 진행한다고 하기에 애매함
그래서 컴파일러는 cpu가 파이프라인을 효율적으로 쓸 수 있게 명령어를 재배치함
절차지향 함수 내의 코드가 꼭 순서대로 진행되지는 않을 수 있다는 말
근데 이러면 중간에 끼어드는 쓰레드같은 경우 값이 정확하지 않게되는 문제가 생길 수 있음
이런 문제를 해결하기 위해 쓰레드들이 수정순서를 지키면 되고 뭐 원자적이고 어쩍 ㅗ하는데
결국 그래서 연산을 원자적으로 만들면 된다.
원자적이란 원자처럼 쪼갤수 없는 단위를 말하며 cpu가 명령어 1개로 처리하는 연산을 의미
즉 중간에 다른 쓰레드가 끼어들 여지가 없음
원자적 연산은 다른 쓰레드가 끼어들지 않으니 mutex같은것도 필요없고 더 빠르게 실행 가능
c++에서 원자적 연산을 할수 있는 도구로 atomic을 제공
atomic의 메모리 관련 연산에 적절한 memory_order를 지정해서 올바른 결과를 만들 수 있음
동기적 실행 : 프로그램의 실행이 한 갈래로 한단계 처리하면 다음단계로 가는 압식
비동기적 실행 : 프로그램 실행이 한 갈래가 아니라 여러갈래로 갈라져서 동시에 진행되는 것
비동기적으로 하고싶은 일은 어떠한 데이터를 다른 쓰레드를 통해 처리해서 받는 것
쓰레드 t를 사용해서 비동기적으로 값을 받는것은
미래(future)에 t가 원하는 데이터를 돌려주겠다라는 약속(promise)
promise와 future를 사용
/*******************************************************************************************/