Future, Promise
future와 promise는 커뮤니케이션 채널을 만드는 하나의 쌍이다. 이 채널을 통해 전달할 수 있는 신호는 Data, Exception, Signal 그리고 각 promise와 future는 다른 thread에 있어도 되고 같은 thread에서 커뮤니케이션을 해도 된다.

간단하게 코드부터 확인해보자.
Single Thread 예제
Example Code
#include <iostream>
#include <chrono>
#include <future>
int main()
{
// 데이터를 넣을 수 있는 promise 생성
std::promise<int> prms;
// promise와 한 쌍이될 수 있는 future를 get_future함수를 통해 생성
std::future<int> fut = prms.get_future();
// set_value함수를 통해 채널안으로 value 42를 set한다.
prms.set_value(42);
//채널 안으로 들어온 value 42를 다시 뽑아내기 위해 get 함수 사용
int num = fut.get();
std::cout << "num : " << num << std::endl;
return 0;
}

위의 코드는 싱글 Thread에서 관리하는 예제다. 멀티쓰레드 환경일 때 내용을 보자.
Multi Thread 예제
Example Code
#include <iostream>
#include <chrono>
#include <future>
#include <thread>
// 함수의 argument로 promise를 받는다.
void fn(std::promise<int> prm)
{
// set_value함수를 통해 채널안으로 value 42를 set한다.
prm.set_value(42);
}
int main()
{
// 데이터를 넣을 수 있는 promise 생성
std::promise<int> prms;
// promise와 한 쌍이될 수 있는 future를 get_future함수를 통해 생성
std::future<int> fut = prms.get_future();
// 다른 thread에서 함수 fn 실행
std::thread t(fn, std::move(prms));
//채널 안으로 들어온 value 42를 다시 뽑아내기 위해 get 함수 사용
int num = fut.get();
std::cout << "num : " << num << std::endl;
t.join();
return 0;
}

코드를 보면 promise를 std::move()를 통해 RValue로 바꿔서 넘겨주었는데 그 이유는 promise는 복사가 불가능하기 때문이다. 이는 당연하게도 future와 promise는 쌍으로 이루어지는데 promise가 copy가 일어난다면 future promise 쌍(pair)이 깨질 것이다.

promise와 future로 이루어진 커뮤니케이션 채널안에 생성된 thread(t)가 set_value를 통해 42라는 값을 넣었다.
Main Thread는 future의 get함수를 통해 이를 가져온 것이다.
future의 get() 에서 딜레이를 줄 경우
Example Code : future에서 get을 할 때 2초간의 딜레이를 주었을 경우
#include <iostream>
#include <chrono>
#include <future>
#include <thread>
// 함수의 argument로 promise를 받는다.
void fn(std::promise<int> prm)
{
// 2초간 딜레이를 준다.
using namespace std::chrono_literals;
std::this_thread::sleep_for(2s);
// set_value함수를 통해 채널안으로 value 42를 set한다.
prm.set_value(42);
}
int main()
{
// 데이터를 넣을 수 있는 promise 생성
std::promise<int> prms;
// promise와 한 쌍이될 수 있는 future를 get_future함수를 통해 생성
std::future<int> fut = prms.get_future();
// 다른 thread에서 함수 fn 실행
std::thread t(fn, std::move(prms));
//채널 안으로 들어온 value 42를 다시 뽑아내기 위해 get 함수 사용
int num = fut.get();
std::cout << "num : " << num << std::endl;
t.join();
return 0;
}
get 함수를 수행하면 Main thread는 비어있는 커뮤니케이션 채널을 바라보며 wait 상태로 넘어간다. 2초 뒤에 채널안으로 42라는 값이 들어오고 get함수로 block이 되어있던 Main thread는 깨어난 뒤 값을 가져와서 자신의 일을 계속 수행한다. 이러한 동작을 보아 future와 promise 커뮤니케이션 사이에는 condition variable, mutex, shared variable등에 대한 내용이 숨겨져있는 것이다.
그럼 커뮤니케이션 채널에 값이 들어올 때까지 Main thread가 block이 되는 것이 아닌 주기적으로 자신의 일을 수행하면서 값이 준비가 되었는지 체크하는 코드를 만들 수 있다.
future_status 체크
Example Code
#include <iostream>
#include <chrono>
#include <future>
#include <thread>
using namespace std::chrono_literals;
// 함수의 argument로 promise를 받는다.
void fn(std::promise<int> prm)
{
// 2초간 딜레이를 준다.
std::this_thread::sleep_for(2s);
// set_value함수를 통해 채널안으로 value 42를 set한다.
prm.set_value(42);
}
int main()
{
// 데이터를 넣을 수 있는 promise 생성
std::promise<int> prms;
// promise와 한 쌍이될 수 있는 future를 get_future함수를 통해 생성
std::future<int> fut = prms.get_future();
// 다른 thread에서 함수 fn 실행
std::thread t(fn, std::move(prms));
// 0.2초마다 값이 준비가 되었는지 체크한다.
while (fut.wait_for(0.2s) != std::future_status::ready)
{
// 준비가 되지 않았다면 다른 일을 수행한다.
std::cout << "doing other work" << std::endl;
}
const int num = fut.get();
std::cout << "num : " << num << std::endl;
t.join();
return 0;
}

결과를 보면 0.2초마다 doing other work라는 문구가 출력이 되고 2초가 되어 값이 준비가 되고나니 num : 42 문구가 출력된 것을 확인할 수 있다.
set_value vs set_value_at_thread_exit
cppreference를 보면 promise는 set_value와 set_value_at_thread_exit 두 가지 함수가 제공이 되는데 두 함수의 차이를 보자.
set_value()
promise쪽 thread가 자신의 일을 수행할 때 set_value 를 하면 그 순간 커뮤니케이션 채널 안으로 데이터가 들어가고 Signal이 간다. 이 후 자신의 일을 계속 수행하다가 마지막에 Main thread로 join하는 방식으로 동작한다. 즉, set_value를 해서 signal을 보낸 뒤에도 자신의 일을 계속 수행할 수 있게 되는 것이다.
set_value_at_thread_exit
thread 진행 가운데 이 함수를 사용해도 thread는 자신의 일을 계속 진행하다가 마지막에 Main thread에 join이 될 때 자동적으로 값을 set해주고 Signal을 보내는 방식으로 동작한다.
exception 전달
future, promise 쌍은 value 뿐만아니라 exception을 전달하는 것도 가능한데 바로 코드를 보면,
Example Code : exception 전달
#include <iostream>
#include <chrono>
#include <future>
#include <thread>
using namespace std::chrono_literals;
// 함수의 argument로 promise를 받는다.
void fn(std::promise<int> prm)
{
// 2초간 딜레이를 준다.
std::this_thread::sleep_for(1s);
try
{
throw std::runtime_error("error");
}
catch(...)
{
// 이 함수를 사용해 exception 전달
prm.set_exception(std::current_exception());
}
// set_exception을 위에서 했는데 set_value를 수행할 수 없다.
// 즉, set_exception와 set_value 중 하나만 수행해야한다.
// 여기서는 exception예제이므로 set_value 주석 처리
// prm.set_value(42);
}
int main()
{
// 데이터를 넣을 수 있는 promise 생성
std::promise<int> prms;
// promise와 한 쌍이될 수 있는 future를 get_future함수를 통해 생성
std::future<int> fut = prms.get_future();
// 다른 thread에서 함수 fn 실행
std::thread t(fn, std::move(prms));
// exception 잡기
try
{
const int num = fut.get();
std::cout << "num : " << num << std::endl;
}
catch(const std::exception& e)
{
std::cerr << "exception : " << e.what() << '\n';
}
t.join();
return 0;
}

정리하자면 확실히 mutex, cv 등을 사용하는 것보다 더 편하게 value, exception, signal을 보낼 수 있었다. 그런데 문제는 라이브러리 구현에 따라 다르겠지만 future, promise 속도가 약간 느리다는 것이다. 이유는 커뮤니케이션 채널을 만들기 위해 내부적으로 Heap memory allocation, mutex, cv(contion variable), reference couter 등을 다뤄야하기 때문이다.
결론
결론적으로 성능이 중요하지 않은 케이스 혹은 중요하더라도 전체 워크플로우에서 한 두번만 호출된다면 사용해도 될 것이라고 한다. 가독성이 뛰어나기 때문에 읽기 쉬운 코드가 만들어지지만 성능적으로 문제가 된다면 future, promise를 피하고 다른 최적화된 concurrency 라이브러리를 사용하는 것이 좋겠다.
'C++ > Thread (Async)' 카테고리의 다른 글
[Async] std::packaged_task (0) | 2022.08.14 |
---|---|
[Async] std::async (0) | 2022.08.14 |
[Async] shared_future (0) | 2022.08.11 |
[Async] 비동기 함수 Introduction (0) | 2022.08.10 |