C++ 쓰레드 -4

·2022년 7월 12일
0

cpp_study

목록 보기
25/25

비동기 연산을 위한 도구들

동기(synchronous)와 비동기(asynchronous) 실행

하드 디스크에서 파일을 읽는다고 생각해 보자.
SSD가 아니라 하드 디스크를 사용하면 임의의 위치에 쓰여져 있는 파일을 읽는데 시간이 오래 걸림. -> 램에서 데이터를 읽어내는 데 50나노초가 걸리는데, 그에 비해 약 8만배 느림.

동기적인 작업

string txt = read("a.txt"); // 5ms
string result = do_something_with_txt(txt); // 5ms
do_other_computation(); // 5ms 걸림 (CPU 로 연산을 수행함)

위 코드가 순차적으로 실행된다고 하자.

1번에 1개씩 순차적으로 실행되는 작업을 동기적(synchronous)으로 실행된다고 부름
동기적인 작업들은 한 작업이 끝날 때까지 다음 작업으로 이동하지 않기 때문.

비동기적인 작업

프로그램의 실행이 한 갈래가 아니라 여러 갈래로 갈라져서 동시에 진행되는 것

void file_read(string* result) {
  string txt = read("a.txt"); // (1)
  *result = do_something_with_txt(txt);
}

int main() {
  string result;
  thread t(file_read, &result);
  do_other_computation(); // (2)
  t.join();
}
  1. (1) 파일 입출력을 맡겨 두다가,
  2. CPU 놀지 않고 (2) 먼저 하다가 스레드 끝남.
  3. 이후 join 수행으로 다시 file_read 스레드를 실행,
  4. 하드 디스크에서 a.txt 파일의 내용이 도착해 있으므로, do_something_with_txt를 바로 실행함.

JS와는 달리 명시적으로 쓰레드를 생성해서 적절히 수행해야 했음
-> C++11 도입 이후 비동기적 실행을 할 수 있게 해주는 도구를 제공해 줌.

std::promise 와 std::future

어떤 데이터를 다른 스레드를 통해 처리해서 받아내는 것.

어떤 스레드 T를 사용해서, 비동기적으로 값을 받아내겠다
: 미래에(future) 쓰레드 T가 원하는 데이터를 돌려 주겠다 라는 약속(promise)라고 볼 수 있음.

#include <future>
#include <iostream>
#include <string>
#include <thread>

using std::string;

void worker(std::promise<string>* p) {
  // 약속을 이행하는 모습. 해당 결과는 future 에 들어간다.
  p->set_value("some data");
}

int main() {
  std::promise<string> p;
  
  // 미래에 string 데이터를 돌려 주겠다는 약속.
  std::future<string> data = p.get_future();
  std::thread t(worker, &p);
  
  // 미래에 약속된 데이터를 받을 때 까지 기다린다.
  data.wait();
  
  // wait 이 리턴했다는 뜻이 future 에 데이터가 준비되었다는 의미.
  // 참고로 wait 없이 그냥 get 해도 wait 한 것과 같다.
  std::cout << "받은 데이터 : " << data.get() << std::endl;
  t.join();
}

promise 객체를 정의할 때, 연산을 수행하고 돌려줄 객체의 타입을 템플릿 인자로 받음.
우리의 경우 string 객체를 돌려줄 예정이므로 string을 전달함.

❗ future에서 get 함수 호출하면 설정된 객체가 이동해 절대로 get을 2번 호출하면 안됨.

promise: 생산자 - 소비자 패턴에서 마치 생산자(producer)의 역할을 수행하고, future는 소비자(consumer)의 역할을 수행한다고 볼 수 있음.

shared_future

여러 개의 스레드에서 future를 get하고 싶을 때 사용

packaged_task

promise-future 패턴을 비동기적 함수(Callable)의 리턴값에 간단히 적용할 수 있는 packaged_task 라는 것을 지원.

future는 packaged_task가 리턴하는 future에서 접근할 수 있음.

int some_task(int x) { return 10 + x; }
int main() {
  // int(int) : int 를 리턴하고 인자로 int 를 받는 함수. (std::function 참조)
  std::packaged_task<int(int)> task(some_task);
  std::future<int> start = task.get_future();
  
  std::thread t(std::move(task), 5);
  std::cout << "결과값 : " << start.get() << std::endl;
  t.join();
}

packaged_task는 비동기적으로 수행할 함수 자체를 생성자의 인자로 받음.
템플릿 인자로 해당 함수의 타입을 명시해야 함.

packaged_task는 전달된 함수를 실행해서, 그 함수의 리턴값을 promise에 설정함.

이때 packaged_task는 복사 생성이 불가능하므로, 명시적으로 move해 줘야 함.
쓰레드에 굳이 promise를 전달하지 않아도 알아서 packaged_task가 함수의 리턴값을 처리해줘서 매우 편리함.

std::async

std::async 에 어떤 함수를 전달한다면, 아예 쓰레드를 알아서 만들어서 해당 함수를 비동기적으로 실행하고, 그 결과값을 future 에 전달함.

std::future<int> lower_half_future =
std::async(std::launch::async, sum, cref(v), 0, v.size() / 2);

async 함수는 인자로 받은 함수를 비동기적으로 실행한 후, 해당 결과값을 보관할 future를 리턴함.

첫번째 인자로 아래 두가지 경우가 가능

  • std::launch::async : 바로 쓰레드를 생성해서 인자로 전달된 함수를 실행한다.
  • std::launch::deferred : future 의 get 함수가 호출되었을 때 실행한다. (새로운 쓰레드를 생성하지 않음), 즉 동기적으로 실행됨
#include <future>
#include <iostream>
#include <thread>

int do_work(int x) {
  // x 를 가지고 무슨 일을 한다.
  std::this_thread::sleep_for(std::chrono::seconds(3));
  return x;
}

void do_work_parallel() {
  auto f1 = std::async([]() { do_work(3); });
  auto f2 = std::async([]() { do_work(3); });
  do_work(3);
  f1.get();
  f2.get();
}

void do_work_sequential() {
  do_work(3);
  do_work(3);
  do_work(3);
}

int main() { do_work_parallel(); }

3 개의 do_work 함수를 동시에 각기 다른 쓰레드에서 실행한 덕분에 3 초 만에 끝난다.

profile
이것저것 개발하는 것 좋아하지만 서버 개발이 제일 좋더라구요..

0개의 댓글