SW 그리고 아빠 엔지니어링 중...

아는 만큼 보이고, 경험해봐야 알 수 있고, 자꾸 써야 내 것이 된다.

L C++/Concurrency

[Concurrency] Promise /Future 모델 필요성 및 사용 방법

보리남편 김 주부 2023. 10. 25. 01:36

Promise / Future 모델의 필요성


이전 글에서 기존 함수를 thread로 대체하는 방법을 알아봤었는데 일반 함수는 보통 리턴 값으로 결과를 반환하지만 스레드는 return을 지원하지 않기에(주 스레드(메인 스레드)로 결과 값을 전달할 수 없기에) out parameter를 사용했었다.

2023.07.24 - [언어/C++] - [Thread] 인자와 callable object

새로운 스레드로 실행이 되긴 했지만 결국 연산이 종료되어도 새로운 스레드가 종료되기 전까지 기다려야 하는 응답대기 시간이 발생하는데 이를 해결하기 위한 promise /future 모델을 사용해 보자.

 

예제 1
아래 예제는 2개의 값을 받아 합산한 결과를 리턴하는 add2() 함수를 스레드로 실행시켜 그 결과 값을 's'로 메인스레드에서 받으며 add2()는 연산 후 3초의 sleep()이 있는 코드이다.
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
using namespace std::literals;

//최초 함수
int add1(int a, int b)
{
	int s = a + b;
	return s;
}

//return 값 대신 결과를 받기위한 out parameter 사용
void add2(int a, int b, int& s)
{
	std::cout << "start" << std::endl;
	s = a + b;

	// 연산은 종료 되었지만.. 마무리 작업에 시간이 걸린다고 가정
	std::this_thread::sleep_for(3s);
}

int main()
{
	int s = 0;
	std::thread t(add2, 10, 20, std::ref(s));

	t.join();

	std::cout << "end!" << std::endl << "s = " <<s << std::endl;
}

 

우선 실행결과를 보자

결과

 

:위 예제코드에서는 add2()에서 얻고자 하는 연산은 이미 완료되었지만 스레드가 종료되기 전까지는 주 스레드에 out parameter를 전달할 수 없으며(응답 대기시간이 발생) 이 문제를 해결하기 위한 모델이 바로 promise /future이다.

 

Promise / Future 모델 사용 방법


주 스레드에서 Promise / Future 사용 시

promise / future 모델 사용 방법에 대한 이해를 돕기 위해 주 스레드에서 사용하면 아래 순서대로 사용해야 한다.

std::promise<int> pr;                         // promise 객체 생성
std::future<int> ft = pr.get_future();   // promise 안에서 future 객체를 꺼낸다.
p.set_value(s);                                  // promise에 data를 넣기
int ret = ft.get();                                 // future에서 data 얻기 

 

새로운 스레드와 함께 비동기식으로 promise / future를 사용하면 조금 헷갈릴 수도 있는데 주석의 step 순서에 따라 확인해 보자.

예제 2
다음 예제는 위 예제 1 코드에 promise /future 모델을 적용한 코드이다.
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
using namespace std::literals;

// promise / future 모델 사용법

void add(int a, int b, std::promise<int>& p)
{
	std::cout << "start!" << std::endl;
	int s = a + b;

	// step 5. 연산이 종료 되었으면 promise 를 사용해서 data를 전달한다.
	p.set_value(s);
    // 이 때 아까 step 4에서 대기중이던 주 스레드가 깨어난다.

	std::this_thread::sleep_for(3s);
}

int main()
{
	// step 1. promise 객체 생성
	std::promise<int> pr; 

	// step 2. promise 안에서 future 객체를 꺼낸다.
	std::future<int> ft = pr.get_future();

    // step 3. 결국 연산은 새로운 스레드에서 할 것이고 결과는 
            // 주 스레드에서 받아서 처리하기 위해 promise 객체를 참조로 전달
	std::thread t(add, 10, 20, std::ref(pr));


	// step 4. promise에서 전달한 data가 있으면 즉시 꺼내고 없으면 대기한다.
	int ret = ft.get();

	// step 6. 이후 코드가 실행된다.
	std::cout << "ret = " << ret << std::endl;

	t.join();
	std::cout << "end!" << std::endl;
	
}
실행 결과

promise / future 모델 사용

: 예제 1 코드와 달리 step 4에서 연산 결과 값을 얻게 되면 스레드가 종료되기 이전에 주 스레드로 응답이 일어난다.

 

Promise / Future 모델 콘셉트

promise와 future는 하나의 쌍을 이루며 비동기적으로 커뮤니케이션 채널을 만드는 모델이다. 채널을 통해 data, exception, signal공유받을 수 있는데 (이름에서도 알 수 있듯이 Promise(약속하다)에 data를 넣고 future(미래)에서 가져가는 방식이다.) 다만 promise와 future는 페어 콘셉트이기에 깊은 복사를 허용하지 않는다. 당연한 얘기일 수도 있는데 복사를 하는 순간 페어의 관계가 깨지기 때문이다. promise의 내부 복사 정의를 보면 아래 방법만 허용하고 있다. (&&(rvalue reference) 혹은 참조(&))

promise& operator=( promise&& other ) noexcept;
(1) (since C++11)
promise& operator=( const promise& rhs ) = delete;
(2) (since C++11)

 

그래서 예제 2 코드는 아래코드로 변환이 가능하다.

//	std::thread t(add, 10, 20, std::ref(pr));
	std::thread t(add, 10, 20, std::move(pr));

: 그리고 이렇게 변환해도 상관없는 이유는 주 스레드에 이후 코드를 보면 더 이상 promise를 사용하지 않기에  promise 소유권을 새로운 스레드로 넘겨도 상관없다.

 

Promise 예외 전달


예외는 스레드 단위로 동작하며, 기본적으로 새로운 스레드에서 던진 예외는 주 스레드에서 받을 수 없다.

하지만 promise로는 예외를 전달할 수 있다. 우선 아래 예제코드를 실행해 보자.

 

예제 3
다음 예제는 입력된 두 수 a, b를 a/b로 나누는 함수 divide()를 스레드로 실행하며, b의 값을 '0'으로 전달하여 예외상황을 발생시키는 코드이다.
#include <iostream>
#include <thread>
#include <future>

void divide(std::promise<int>&& p, int a, int b)
{
    try
    {
        if (b == 0)
        {
            throw std::runtime_error("divide by zero");
        }
        p.set_value(a / b);
    }
    catch (...)
    {
        //p.set_exception( std::current_exception() );
    }
}

int main()
{
    std::promise<int> pm;
    std::future<int>  ft = pm.get_future();

    //예외를 발생시키기 위한 코드
    std::thread t(divide, std::move(pm), 10, 0);

    try
    {
        int ret = ft.get();
    }
    catch (std::exception& e) // C++ 표준 예외의 최상의 클래스
    {
        std::cout << "예외 발생 : " << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "알수 없는 예외 발생\n";
    }
 
    t.join();
}
결과
예외발생 : broken promise

그냥 예외처리 시에는 주스레드에서 에러를 받아 출력을 하긴 하지만 새로운 스레드에서 남긴 메시지 "divide by zero"는 출력하지 못한다. 아래코드를 수정하고 다시 실행해 보자.

 

p.set_exception( std::current_exception() );

 

결과
예외발생 : divide by zero

: 새로운 스레드에서 promise로 전달한 예외 메시지가 주 스레드에 전달된 것을 확인할 수 있다.

 

참고


Promise Member functions

constructs the promise object
(public member function)
destructs the promise object
(public member function)
assigns the shared state
(public member function)
swaps two promise objects
(public member function)
Getting the result
returns a future associated with the promised result
(public member function)
Setting the result
sets the result to specific value
(public member function)
sets the result to specific value while delivering the notification only at thread exit
(public member function)
sets the result to indicate an exception
(public member function)
sets the result to indicate an exception while delivering the notification only at thread exit
(public member function)

 

기타

C++11부터 사용할 수 있으며, <future>에 정의되어 있다.

 

참고


* 본 글에 예제 코드는 코드누리 교육을 받으면서 제공된 샘플코드를 활용하였습니다.

728x90