[C++] async, promise, future, task의 사용법


Study/C , C++ , MFC  2020. 4. 10. 19:27

안녕하세요. 명월입니다.


이 글은 C++에서의 async, promise, future, packaged_task의 사용법에 대한 글입니다.


이전에 제가 쓰레드에 대해서 간략하게 설명한 적이 있습니다.

링크 - [C++] 쓰레드(Thread)를 사용하는 방법


쓰레드란 main 함수에서 병렬 처리가 가능한 것을 뜻하다고 설명했습니다. 그리고 각 쓰레드에서 사용되는 값을 동기화 하기 위해서는 mutex의 lock를 이용합니다.

그런데 이번에는 같은 병렬처리이기는 하는데 값을 동기화하여 공유하는 것이 아니라 함수처럼 결과 return을 기다리는 식으로 작성하고 싶을 때가 있습니다.


그럴 때는 이 promise와 future를 이용하면 각 병렬 처리간에 값을 주고 받기가 쉽습니다.

#include <stdio.h>
#include <iostream>
// promise와 future를 이용하기 위한 라이브러리
#include <future>
// thread를 이용하기 위한 라이브러리
#include <thread>
using namespace std;
// 쓰레드를 위한 함수 (파라미터는 결과를 받기 위한 promise, 반복문의 횟수)
void thread_function(promise<int>* p, int count)
{
  // 결과 변수
  int ret = 0;
  // 반복문의 횟수만큼 더한다.
  for (int i = 0; i < count; i++)
  {
    // count가 10이면 0부터 10까지 더하는 함수이다.
    ret += i;
  }
  // 결과를 set_value로 넘긴다.
  p->set_value(ret);
}
// 실행 함수
int main()
{
  // promise 선언
  promise<int> p;
  // promise와 future를 연결.
  // promise에서 set_value를 하면 future에서 get으로 받는 것이다.
  future<int> data = p.get_future();
  
  // 쓰레드를 실행하여 promise를 던지고, 반복문의 횟수를 10을 설정한다.
  thread _th(thread_function, &p, 10);
  
  // data.get()을 하면 promise가 set_value할 때까지 기다린다.
  // 내부적으로 data.wait이 실행된다.
  cout << data.get() << endl;
  
  // thread를 종료한다.
  _th.join();
  return 0;
}

여기서 값을 주고 받는 기능을 하는 건 promise의 set_value함수와 future의 get함수입니다.

javascript의 promise를 생각해서 promise의 set_value함수를 두번 호출하면 에러가 발생합니다.

future의 경우도 get을 여러번 호출하면 에러가 발생합니다..일회용입니다.그러나 future만은 재사용이 가능한 객체가 있습니다.

#include <stdio.h>
#include <iostream>
// promise와 future를 이용하기 위한 라이브러리
#include <future>
// thread를 이용하기 위한 라이브러리
#include <thread>
using namespace std;
// 쓰레드를 위한 함수 (파라미터는 결과를 받기 위한 promise, 반복문의 횟수)
void thread_function(promise<int>* p, int count)
{
  // 결과 변수
  int ret = 0;
  // 반복문의 횟수만큼 더한다.
  for (int i = 0; i < count; i++)
  {
    // count가 10이면 0부터 10까지 더하는 함수이다.
    ret += i;
  }
  // 결과를 set_value로 넘긴다.
  p->set_value(ret);
}
// 실행 함수
int main()
{
  // promise 선언
  promise<int> p;
  // promise와 share_future를 연결.
  // share_future의 경우는 get을 한번만 사용하는 것이 아니고 여러번 사용가능합니다.
  share_future<int> data = p.get_future();
  
  // 쓰레드를 실행하여 promise를 던지고, 반복문의 횟수를 10을 설정한다.
  thread _th(thread_function, &p, 10);
  
  // data.get()을 하면 promise가 set_value할 때까지 기다린다.
  cout << data.get() << endl;
  cout << data.get() << endl;
  cout << data.get() << endl;
  cout << data.get() << endl;
  
  // thread를 종료한다.
  _th.join();
  return 0;
}

참조 - http://www.cplusplus.com/reference/future/promise/


여기서 promise와 future를 사용하기 위해서는 결국 함수에 파라미터로 promise를 설정해야 합니다.

기존에 있는 함수를 수정할 수 없는 상황에서 비동기를 사용하려고 한다면 promise를 사용할 수 없겠습니다.


이걸 극복한 것이 packaged_task입니다.

#include <stdio.h>
#include <iostream>
// promise와 future를 이용하기 위한 라이브러리
#include <future>
// thread를 이용하기 위한 라이브러리
#include <thread>
using namespace std;
// 쓰레드를 위한 함수 (promise 파라미터를 빼고 일반 함수 형태이다.)
int thread_function(int count)
{
  int ret = 0;
  // count까지 더한다
  for (int i = 0; i < count; i++)
  {
    ret += i;
  }
  // 결과 리턴
  return ret;
}
// 실행 함수
int main()
{
  // packaged_task 선언
  packaged_task<int(int)> task(thread_function);
  // packaged_task와 share_future를 연결.(promise와 비슷한다.)
  shared_future<int> data = task.get_future();
  // move함수를 사용하여 task를 쓰레드에 넘긴다.
  thread _th(move(task), 10);
  
  // data.get()을 하면 함수가 종료될 때까지 기다린다.
  cout << data.get() << endl;
  cout << data.get() << endl;
  cout << data.get() << endl;
  cout << data.get() << endl;

  _th.join();
  return 0;
}

위 promise와 packaged_task의 결과는 같습니다. 그러나 쓰레드를 위한 함수의 리턴과 파라미터 타입이 바뀌었습니다. packaged_task를 사용하면 이전에 작성했던 함수들도 수정없이도 병렬 처리가 가능하다는 뜻입니다.

참조 - http://www.cplusplus.com/reference/future/packaged_task/


여기서 이 promise와 packaged_task의 최종 보스는 async입니다.

async란 녀석은 promise나 packaged_task, thread 객체까지 전부 다 생략해 버린 것입니다.

#include <stdio.h>
#include <iostream>
// promise와 future를 이용하기 위한 라이브러리
#include <future>
// thread를 이용하기 위한 라이브러리
#include <thread>
using namespace std;
// 쓰레드를 위한 함수 (promise 파라미터를 빼고 일반 함수 형태이다.)
int thread_function(int count)
{
  int ret = 0;
  // count까지 더한다
  for (int i = 0; i < count; i++)
  {
    ret += i;
  }
  // 결과 리턴
  return ret;
}
// 실행 함수
int main()
{
  // async에 함수명과 파라미터를 설정, 리턴은 future이다.
  shared_future<int> data = async(thread_function, 10);
  // data.get()을 하면 함수가 종료될 때까지 기다린다.
  cout << data.get() << endl;
  cout << data.get() << endl;
  cout << data.get() << endl;
  cout << data.get() << endl;

  return 0;
}

역시 결과는 같습니다. 보면 소스가 더 단순해 지고 소스 스텝이 더 줄었습니다. 여기서 람다식까지 넣으면...

#include <stdio.h>
#include <iostream>
// promise와 future를 이용하기 위한 라이브러리
#include <future>
// thread를 이용하기 위한 라이브러리
#include <thread>
using namespace std;
// 실행 함수
int main()
{
  // async에 람다식을 넣고, 파라미터는 10을 넣었습니다. 결과는 future로 받으니 get을 바로 호출하면 함수값이 바로나온다.
  int ret = async([](int count) -> int {
    int ret = 0;
    for (int i = 0; i < count; i++)
    {
      ret += i;
    }
    return ret;
  }, 10).get();
  // 콘솔 출력
  cout << "ret - " << ret << endl;

  return 0;
}

참조 - http://www.cplusplus.com/reference/future/async/?kw=async

async까지 오니깐 c++소스가 무슨 javascript 소스처럼 변해 버렸네요.

promise와 future, packaged_task, async는 제가 c++를 한창 할 때도 모르던 키워드이고 객체입니다.

우연히 thread pool에 대해서 조사하던 중, async 키워드가 있는 것을 발견하고 파고들다 보니 여기까지 알게 되었네요.. 참 c++를 한창 다룰 때, 이런 기능을 알았으면 참 좋았을 텐데라는 아쉬움이 있네요.


여기까지 C++에서의 async, promise, future, task의 사용법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.