C++ Chapter 19.3 : std::thread와 멀티쓰레딩 기초

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!


chapter 19. 모던 C++ 필수 요소들

std::thread와 멀티쓰레딩의 기초

  • C++ 11 에 도입 되었다.
  • 현대 컴퓨터들은 거의 다 멀티 코어 CPU를 가진다.
  • 프로그래머는 멀티 코어를 활용할 수 있는 능력이 필요하다.

🔔 멀티 쓰레딩의 개념과 원리

Process

OS가 우리가 작성할 프로그램을 실행시킬 때 관리를 하는 단위

  • 하나의 프로세스가 여러개의 쓰레드를 관리할 수 있다.

Multi Processing

여러개의 CPU가 동시에 여러개의 프로세스를 수행함

코어가 하나만 존재할 경우엔 하나의 일밖에 처리를 못한다. 여러 프로세서(CPU) 상에서 여러 프로세스가 동시에 병렬 처리로 실행되는 것을 Multi Processing 이라고 한다. 예를 들어 듀얼 코어 이런 것들!

내 CPU는 코어가 몇 개인지 확인해보기

image

작업 관리자(Ctrl + Alt + Delete) - 성능

  • 코어
    • 내 CPU 의 물리적 코어 개수, 난 2개
  • 논리 프로세스
    • 난 4개
    • 물리적인 코어의 개수의 약 2배다.
    • 프로세서가 4개인것처럼 일을 하게끔 하고 있기 때문
  • 이용률
    • CPU 논리 프로세서들을 고려한 현재 사용중인 CPU 비율
  • 프로세스 개수 < 스레드 개수 인 것을 확인할 수 있는데 하나의 프로세스가 여러개의 스레드를 사용하는 경우가 많기 때문이다.


Multi Threading

하나의 프로그램이 여러개의 Thread로 구성되어있는 형태

  • 하나의 프로그램이 여러 서브 프로그램으로 구성되어 있는 형태
  • 마치 여러 Thread 들이 동시에 실행되는 것처럼 번갈아 가며 동작한다.
    • 하나의 프로세서(CPU)에서 여러 프로세서가 동시에 실행되는 것 같은 효과를 낸다.
    • 여러개의 스레드들이 하나의 메모리를 공유한다.
      • 위험하기도 하고 편하기도 하다.

image

위 그림은 여러개의 스레드로 이루어진 하나의 프로세스의 실행 과정을 나타낸다.

  • Main Thread
    • main 함수의 시작이 실행되는 스레드
      • 따라서 가장 먼저 실행 됨
    • 부가적인 스레드들을 순차적으로 실행시킨다
  • 각각 스레드들은 각자 맡은 작업들을 독립적으로, 병렬적으로 수행한다.
    • Main Thread 가 작업 1Thread 0 에게 맡긴다.
    • Main Thread 가 작업 2Thread 1 에게 맡긴다.
    • Main Thread 가 작업 3Thread 2 에게 맡긴다.
  • 각각 스레드들이 각자 맡은 작업들을 끝낼 때 까지 Main Threadwait 기다린다.
    • 각 스레드들의 작업들이 다 끝났다면 Main Thread는 스레드들에게 맡길 필요까진 없는 가벼운 추가작업을 마친 후 종료한다.


🔔 Thread 생성하기

#include <iostream>
#include <thread>

using namespace std;

int main()
{
	cout << std::thread::hardware_concurrency() << endl;
	cout << std::this_thread::get_id() << endl;

	std::thread t1 = std::thread([] {
	cout << std::this_thread::get_id() << '\n';
	//while (true);
	});
	t1.join();		// t1이 끝날 때까지 기다린다.
}
💎출력💎

4
17952
16024

#include <thread>

  • 논리 프로세서
    • std::thread::hardware_concurrency()
      • 내 컴퓨터의 논리프로세서가 몇개인지를 리턴한다.
  • 스레드를 생성하고 할 일 부여하기
    • std::thread t1
      • 블라블라 일을 수행하는 t1 스레드.
    • std::thread(블라블라)
      • 스레드의 할일(함수)을 std::thread()()안에 인수로 넣어주면 된다.
      • 스레드에게 할일을 함수 포인터, std::function, 람다 함수 를 통해 알려줄 수 있다.
        • 함수에 필요한 인수도 같이 넘겨줄 수 있다.
          • std::thread t1 = std::thread(func, 123, 456) 이런식으로.
    • t1에게 [] { cout << std::this_thread::get_id() << '\n';} 를 수행하는 작업을 부여 한다. (람다 함수)
      • 이 스레드는 자신의 id 를 출력하는 일을 함
  • 스레드의 id
    • std::this_thread::get_id()
      • this_thread 👉 이 일을 수행하는 지금 스레드
      • 이것을 실행시킨 스레드의 id를 리턴한다.
    • 출력 결과를 통해 main 함수에서 실행한 Main Thread의 id 는 17952 인 것을 확인할 수 있고, t1 스레드의 id 는 16024 인 것을 확인할 수 있다. 두 스레드가 별개의 스레드!
  • join 함수
    • t1.join() 👉 t1스레드가 다 끝날 때 까지 Main Thread가 기다려준다.

      • 만약 t1.join()도 없고 아래 코드와 같이 t1이 하는 작업에 무한 루프 작업이 있는 상태였다면 t1 스레드와 Main 스레드가 서로 의사 소통이 되지 않아 t1이 무한 반복에 있는 도중에 Main 스레드가 종료되어 버릴 수 있다.
      • t1.join()으로 Main 스레드에게 t1이 끝날 때까지 기다려야 한다는 것을 알려주면 Main 스레드가 t1 스레드가 맡은 작업이 다 끝날 때까지 기다려준다.
        std::thread t1 = std::thread([] { 
            cout << std::this_thread::get_id() << '\n'; 
            while (true); 
        });
        t1.join();		// t1이 끝날 때까지 기다린다.
        


🔔 여러개의 Thread 생성하기

생길 수 있는 문제

#include <iostream>
#include <thread>

using namespace std;

int main()
{
	std::thread t1 = std::thread([] {
		cout << std::this_thread::get_id() << '\n';
     while(true);
	});

	std::thread t2 = std::thread([] {
		cout << std::this_thread::get_id() << '\n';
     while(true);
	});

	std::thread t3 = std::thread([] {
		cout << std::this_thread::get_id() << '\n';
     while(true);
	});

	std::thread t4 = std::thread([] {
		cout << std::this_thread::get_id() << '\n';
     while(true);
	});

	t1.join();
	//t2.join();
	//t3.join();
	//t4.join();
}
💎출력💎

11212
463276042468

9376


스레드는 현재 t1, t2, t3, t4 이렇게 4 개이고 다 무한 루프 작업을 해서 프로그램이 끝나지 않는다.

  • 넷 다 무한루프 작업을 하므로 joint1.join() 하나만 있어도 무방함
    • 메인 스레드가 t1 스레드가 끝나기를 기다림
  • 스레드 id 가 463276042468 이렇게 이상하게 나온 이유
    • t1, t2, t3, t4 스레드가 각각 병렬로 동시에 실행되기 때문에(멀티스레딩) id가 중구난방으로 섞여 출력됨
      • cout이 감당을 못함 ✨ 막 섞여서 출력 됨..


해결 시도 1 : vector에 스레드 넣기 ❌

위와 같이 스레드를 여러개 만들어 vector에 넣어 작업한다면?

#include <iostream>
#include <thread>

using namespace std;

int main()
{
	const int num_pro = std::thread::hardware_concurrency();

	vector<std::thread> my_threads;
	my_threads.resize(num_pro);

  for (auto & e : my_threads)
  {
      e = std::thread([](){
				cout << std::this_thread::get_id() << endl;
				while(true){}
          });
  }
		
	for (auto & e : my_threads)
		e.join();
}
  • num_pro
    • 내 컴퓨터의 논리프로세서 개수
      • hardware_concurrency() 의 리턴 값
  • vector<std::thread> my_threads
    • 스레드들을 담을 벡터
  • my_threads.resize(num_pro)
    • 벡터 사이즈를 논리프로세서 개수와 일치시킴. 아직 할일이 부여 되지 않은 스레드들이 num_pro개만큼 vector에 자리 잡게 됨.
      • 우리가 실행시킬 스레드와 하드웨어 논리프로세서 개수와 일치시키는 것이 일반적이다
        • 그러나 작성하는 프로그램 성질에 따라 일치 하지 않도록 설정할 수도 있음
  • 첫번째 for문
    • my_threads 벡터의 스레드들에게 차례대로 할일을 부여하고 실행시킴
      • 스레드 본인의 id 를 출력하고 무한 루프를 돌리는 작업을 수행 하는 일
  • 두번째 for 문
    • 각각의 스레드들을 기다림

image

  • 스레드들을 벡터에 넣어두고 for문을 통해 차례 차례 실행시켰음에도 불구하고 여전히 문제가 해결되지 않았다.
    • 중구난방으로 스레드 id가 출력되는 것을 볼 수 있다. 여러 id 가 섞여 출력되고 빈칸들도 보인다.
    • 심지어 스레드를 논리 프로세서 개수만큼 돌리니 CPU 사용량이 100%가 나온다!


해결 시도 2 : sleep_for 함수를 통해 스레드를 잠시 쉬게 하기 ❌

#include <iostream>
#include <thread>
#include <mutex>		// semaphore는 없음
using namespace std;

int main()
{
	auto work_func = [](const string & name)
	{
		for (int i = 0; i < 5; ++i)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			
			cout << name << " " << std::this_thread::get_id() << " is working " << i << endl;
		}
	};
	work_func("JackJack");
	work_func("Dash");
}

image

해결 된 듯 보이지만 사실 이건 멀티 스레딩이 아니다.

  • work_func 포인터를 통해 사용 가능한 이 람다 함수
  • sleep_for 함수 this_thread::sleep_for(chrono::milliseconds(100))
    • 지금 이 스레드(this_thread)를 100 밀리 세컨즈 시간동안만 쉬게 한다.
  • 스레드 id 들이 이전처럼 중구난방이 아닌 각각의 스레드마다 제대로 잘 출력되는 것을 볼 수 있다!
    • 그러나 모든 스레드 id가 14308로 동일한 것을 볼 수 있다. 동일한 스레드.
      • 14308 스레드가 work_func("JackJack")을 실행시키고 i = 0 에서 쉬는 동안 work_func("Dash") i = 0 실행. 다시 work_func("Dash") 작업을 쉬는동안 work_func("JackJack") 의 출력을 실행하고 i = 1 … 이런식!
  • 이건 멀티 스레딩이 아니며 비효율적이다
    • 멀티스레딩은 한번에 여러 스레드가 각자의 작업들이 동시에 실행되는 것!
#include <iostream>
#include <thread>

using namespace std;

int main()
{
	auto work_func = [](const string & name)
	{
		for (int i = 0; i < 5; ++i)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			
			cout << name << " " << std::this_thread::get_id() << " is working " << i << endl;
		}
	};
	std::thread t1 = std::thread(work_func, "JackJack");
	std::thread t2 = std::thread(work_func, "Dash");

    t1.join();
    t2.join();
}

image

멀티 스레드로 실행

  • work_func("JackJack")만을 담당하는 스레드와 work_func("Dash")만을 담당하는 스레드 이렇게 따로두고 메인 스레드가 두 스레드가 작업을 끝내기를 기다린다.
    • 두 스레드의 id가 다름
  • 출력 결과를 보면 두 스레드 작업이 동시에 일을 실행하는 것을 볼 수 있다.
    • 출력 결과가 살짝 섞여 있다
      • 👉 출력만큼은 번갈아 가며 출력하게끔 하고 싶을 때 해결책 mutex


해결책 : mutex

#include <mutex>

  • mutal exclusion 의 약자. 상호 배제.
#include <iostream>
#include <thread>
#include <mutex>  // ✨✨

using namespace std;

int main()
{
    mutex mtx;
    
	auto work_func = [](const string & name)
	{
		for (int i = 0; i < 5; ++i)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			
            mtx.lock();
			cout << name << " " << std::this_thread::get_id() << " is working " << i << endl;
            mtx.unlock();
		}
	};
	std::thread t1 = std::thread(work_func, "JackJack");
	std::thread t2 = std::thread(work_func, "Dash");

    t1.join();
    t2.join();
}

image

  • 이제 출력이 뒤죽박죽 되지 않고 스레드 작업마다 깔끔하게 본인 것이 완전히 출력되는 것을 볼 수 있다.
  • mtx.lock()mtx.unlock() 안에 있는 처리는 A 스레드가 이미 작업중이라면 B 스레드가 이 작업을 처리하지 못하도록 막아준다. 이 부분만큼은 하나의 스레드만 사용할 수 있게끔 잠그는 역할
    mutex mtx;
      
    mtx.lock();
    cout << name << " " << std::this_thread::get_id() << " is working " << i << endl;
    mtx.unlock();
    
  • mtx.lock()
    • 이 부분은 한 스레드만 사용할 수 있게끔 잠금
      • 다른 스레드가 이 출력 부분을 작업중이면 또 다른 스레드가 못 건들도록 잠금
  • mtx.unlock()
    • 잠금을 풀어줌

unlock 까먹으면 큰일 난다! 영원히 lock한 코드에 다른 스레드가 접근할 수 없게 되니까.



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Cpp 카테고리 내 다른 글 보러가기

댓글 남기기