[Modern C++] 15.4. future, promise, packaged_task, async

윤정민·2023년 8월 11일
0

C++

목록 보기
39/46

1. 동기와 비동기 실행

하드 디스크에서 파일을 읽는다고 생각할 때 임의의 위치에 쓰여진 파일을 읽는데 오랜 시간이 걸린다. 이는 read함수가 파일이 하드 디스크에서 읽어지는 동안 기다리기 때문에 read 함수는 파일 읽기가 끝나기 전 까지 리턴하지 않고, CPU는 아무것도 하지않은 채 기다리게 되기 때문이다.

  • 동기적 실행: 한 번에 하나씩 순차적으로 실행, 끝나기 전에 다음 작업으로 이동하지 않음
  • 비동기적 실행: 여러 갈래로 갈라져서 동시에 진행되는 것

2. std::promise, std::future

  • promise: 미래에 쓰레드 T가 원하는 데이터를 돌려주겠다는 약속
  • future: 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();
}
  • 실행 결과
    받은 데이터 : some data

  • std::promise<string> p;
    • 연산 수행 후에 돌려줄 객체의 타입을 템플릿 인자로 받음
  • std::future<string> data = p.get_future();
    • 연산 수행 후 promise객체는 자신이 가지고 있는 future객체에 값을 넣어줌
    • get_future함수를 통해 promise객체에 대응되는 future객체를 얻을 수 있음
  • p->set_value("some data");
    • 실제 연산 결과를 넣어줌

  • promise가 생성자 future이 소비자 역할을 수행한다 할 수 있음
  • 이를 사용해 promise-future 패턴을 구현 가능
    • future에 예외도 전달 가능
  • 예시 코드
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>

std::condition_variable cv;
std::mutex m;
bool done = false;
std::string info;

void worker() {
  {
    std::lock_guard<std::mutex> lk(m);
    info = "some data";  // 위의 p->set_value("some data") 에 대응
    done = true;
  }
  cv.notify_all();
}

int main() {
  std::thread t(worker);

  std::unique_lock<std::mutex> lk(m);
  cv.wait(lk, [] { return done; });  // 위의 data.wait() 이라 보면 된다.
  lk.unlock();

  std::cout << "받은 데이터 : " << info << std::endl;

  t.join();
}
  • wait_for: 정해진 시간 동안만 기다리고 그냥 진행 가능
  • get: 데이터가 이동되기 때문에 단 한번만 수행해야 함

3. shared_future

  • 여러 개의 다른 쓰레드에서 futureget해야하는 경우 사용 가능
  • 데이터를 복사 할 수 있음

4. packaged_task

  • C++에서는 위 promise-future패턴을 비동기적 함수의 리턴값에 간단히 적용할수 있는 packaged_task라는 것을 지원함

  • packaged_task에 전달된 함수가 리턴할 때, 그 리턴값을 promiseset_value하고, 만약에 예외를 던졌다면 promiseset_exception을 수행

  • futurepackaged_task가 리턴하는 future에서 접근할 수 있음

  • 예제코드

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

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();
}
  • 실행 결과
    결과값 : 15

  • 생성
    • packaged_task: 비동기적으로 수행할 함수 자체를 생성자의 인자로 받음
      • 템플릿 인자로 해당 함수의 타입을 명시해야 함
    • task.get_future() :promise에 대응되는 future를 리턴
    std::packaged_task<int(int)> task(some_task);
    std::future<int> start = task.get_future();
  • 쓰레드에 전달
    • 복사 생성이 불가능 하므로 명시적으로 move 해줘야만 함
      thread t(std::move(task), 5);
  • 결과값 얻기
    • futureget함수를 통해 추후에 받을 수 있음
    • 쓰레드에 굳이 promise를 전달하지 않아도 알아서 리턴값을 처리해주기 때문에 편리함
      std::cout << "결과값 : " << start.get() << std::endl;

5. std::async

  • 이전에 쓰레드를 명시적으로 생성해서 실행한 것과 달리 async에 어떤 함수를 전달하면, 쓰레드를 알아서 만들어 해당 함수를 비동기적으로 실행하고 그 결과 값을 future에 전달함

  • 예제 코드

#include <future>
#include <iostream>
#include <thread>
#include <vector>

// std::accumulate 와 동일
int sum(const std::vector<int>& v, int start, int end) {
  int total = 0;
  for (int i = start; i < end; ++i) {
    total += v[i];
  }
  return total;
}

int parallel_sum(const std::vector<int>& v) {
  // lower_half_future 는 1 ~ 500 까지 비동기적으로 더함
  // 참고로 람다 함수를 사용하면 좀 더 깔끔하게 표현할 수 도 있다.
  // --> std::async([&v]() { return sum(v, 0, v.size() / 2); });
  std::future<int> lower_half_future =
    std::async(std::launch::async, sum, cref(v), 0, v.size() / 2);

  // upper_half 는 501 부터 1000 까지 더함
  int upper_half = sum(v, v.size() / 2, v.size());

  return lower_half_future.get() + upper_half;
}

int main() {
  std::vector<int> v;
  v.reserve(1000);

  for (int i = 0; i < 1000; ++i) {
    v.push_back(i + 1);
  }

  std::cout << "1 부터 1000 까지의 합 : " << parallel_sum(v) << std::endl;
}
  • 실행결과
    1 부터 1000 까지의 합 : 500500

  • 선언
    • 인자로 받은 함수를 비동기적으로 실행한 후, 해당 결과값을 보관할 future를 리턴
    • 첫 번째 인자로 어떤 형태를 실행할지 전달함
      - std::launch::async : 바로 쓰레드를 생성해서 인자로 전달된 함수를 실행
      - std::launch::deferred : future 의 get 함수가 호출되었을 때 실행(새로운 쓰레드 생성x)
      std::future<int> lower_half_future = std::async(std::launch::async, sum, cref(v), 0, v.size() / 2);
  • future리턴
    • 결과값은 async함수가 리턴한 futureget을 통해 얻어낼 수 있음
      return lower_half_future.get() + upper_half;
profile
그냥 하자

0개의 댓글