본문 바로가기
프로그래밍 언어/C++

C++ / std::promise, std::future

by Nighthom 2023. 2. 18.
#include <exception>
#include <future>
#include <iostream>
#include <string>
#include <thread>
using std::string;

void worker(std::promise<string>* p) {
    try {
      throw std::runtime_error("Some Error!");
    } catch (...) {
      // set_exception 에는 exception_ptr 를 전달해야 한다.
      p->set_exception(std::current_exception());
    }
}
int main() {
    std::promise<string> p;

    // 미래에 string 데이터를 돌려 주겠다는 약속.
    std::future<string> data = p.get_future();

    std::thread t(worker, &p);

    // 미래에 약속된 데이터를 받을 때 까지 기다린다.
    data.wait();

    try {
      data.get();
    } catch (const std::exception& e) {
      std::cout << "예외 : " << e.what() << std::endl;
    }
    t.join();
}​

std::future은 std::promise에 data를 넣어주겠다는 약속과 같다. promise 객체에 다른 쓰레드가 데이터를 넣어줄 때 까지 std::future::wait()를 해서 기다릴 수 있고, std::future::get()을 통해 데이터를 가져올 수도 있다. 

 

future::get()을 통해서 데이터를 읽으면 future 내부의 객체가 이동되기 때문에 다시는 접근할 수 없다. 

예외 throw, catch

std::future은 예외도 받을 수 있다. std::future::get()을 하는 시점에 promise에 예외를 std::promise::set_exception(std::current_exception)을 통해서 보내면 예외를 catch할 수 있다. (참고 : std::current_exception은 예외 객체 자신이 아닌 exception의 포인터임을 명심할 것)

#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();
}

wait_for

wait_for은 wait와는 다르게 지정된 시간만큼 대기하고 대기하는 중 promise에 데이터가 오지 않으면 그대로 리턴해버린다. 따라서, 1초정도 wait_for을 걸고 promise가 1초동안 오지 않는다면 바로 다음 코드가 실행된다고 이해하면 된다. 

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

void worker(std::promise<void>* p) {
	std::this_thread::sleep_for(std::chrono::seconds(10));
	p->set_value();
}

int main() {
	std::promise<void> p;

	std::future<void> data = p.get_future();

	std::thread t(worker, &p);

	while (true) {
		std::future_status status = data.wait_for(std::chrono::seconds(1));
		
		if (status == std::future_status::timeout) {
			std::cerr << ">";
		}
		else if (status == std::future_status::ready)
			break;
	}
	t.join();
}

해당 메서드는 future_status 객체를 리턴한다.  std::future_status::timeout은 객체가 준비되지 않았음을, std::future_status::ready는 해당 객체가 준비되었음을 의미한다. 

shared_future

shared_future는 여러 쓰레드에서 get을 해도 promise 객체가 이동되지 않아 여러 쓰레드에서 데이터에 접근할 수 있다. 하나의 쓰레드에서 여러 쓰레드에 데이터를 보낼 때 용이하다.

#include <chrono>
#include <future>
#include <iostream>
#include <thread>
using std::thread;

void runner(std::shared_future<void> start) {
  start.get();
  std::cout << "출발!" << std::endl;
}

int main() {
  std::promise<void> p;
  std::shared_future<void> start = p.get_future();

  thread t1(runner, start);
  thread t2(runner, start);
  thread t3(runner, start);
  thread t4(runner, start);

  // 참고로 cerr 는 std::cout 과는 다르게 버퍼를 사용하지 않기 때문에 터미널에
  // 바로 출력된다.
  std::cerr << "준비...";
  std::this_thread::sleep_for(std::chrono::seconds(1));
  std::cerr << "땅!" << std::endl;

  p.set_value();

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

packaged_task

#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();
}

packaged_task는 promise의 함수 버전같은 것이다. packaged_task에 넣은 함수 객체가 return하면 해당 return값을 promise에 넣어주고, 예외를 throw하면 예외를 전달한다. packaged_task는 복사 생성이 불가능하므로 thread를 생성할 때 std::move()를 활용해 rvalue임을 명시해주어야 한다.

std::async

std::packaged_task나 std::promise같은 경우 명시적으로 쓰레드를 생성해 주어야 했다. 하지만 std::async는 알아서 쓰레드를 생성해 준다. 

#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 = std::async(std::launch::async, some_task, 5);
  // std::future<int> start = std::async(std::async([]() { return some_task(5); }); 
  // 람다 함수로 위와 같이 나타낼 수도 있음.

  std::cout << "결과값 : " << start.get() << std::endl;
  t.join();
}

async에 std::launch::async를 전달하면 객체가 생성되자마자 쓰레드를 생성해서 실행시키고, std::launch::deferred의 경우 future의 get 함수가 호출될 때 쓰레드를 생성한다. 

 

async함수는 함수의 결과값을 포함하는 future을 리턴한다. 해당 값은 future.get을 통해서 얻어볼 수 있다. 

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ Spinlock 구현  (0) 2023.03.11
C++ / std::atomic  (0) 2023.02.17
C++ 11 / mutex를 활용한 동기화  (0) 2023.02.16
C++ 11 / 스레드  (0) 2023.02.16
C++ / std::function  (0) 2023.02.16

댓글