(return이 있는) 기존 함수를 수정 없이 thread로 사용하기(2)😆
참고 : 본 글은 promise/future 모델, packaged_task에 대한 이해를 해야만 std::async를 이해할 수 있기에 두 내용을 모르고 있다면 아래 내용을 공부한 뒤에 본 글을 읽어주기 바란다.
2023.10.25 - [L C++] - [thread] Promise /Future 모델 필요성 및 사용 방법
2023.11.14 - [L C++] - [thread] packaged_task - 기존 함수 그대로 thread에 적용(1)
그럼 기존 함수를 수정 없이 스레드로 동작시킬 수 있는 std::async에 대해 알아보자.
std::async
std::async 🆚 std::packaged_task 차이
std::pakaged_task는 보관해 놨다가 주스레드에서 호출(ex : task(1,2)) 혹은 새로운 스레드에서 수행하지만 std::async는 함수를 즉시 스레드로 수행하며, 별도 스레드(std::thread)로 생성할 필요가 없다.
std::async를 사용한 예제 1를 보자.
예제 1
std::async를 사용하여 add 함수를 수행하는 프로그램 코드이다.
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
using namespace std::literals;
int add(int a, int b)
{
std::this_thread::sleep_for(3s);
return a + b;
}
int main()
{
std::future<int> ft = std::async(&add, 1, 2);
std::cout << "continue main" << std::endl;
int ret = ft.get();
std::cout << "result : " << ret << std::endl;
}
결과
continue main
result : 3
실행결과를 보면 add 함수를 std::thread로 실행하지 않았는데 ft.get()에서 결과 값을 대기하고 있다.
std::async 특징
1. 스레드 풀을 사용하고 있어, 사용자가 std::thread를 사용하지 않아도 std::async() 내부적으로 스레드로 수행한다.
2. std::packaged_task 와 마찬가지로 std::future<>로 결과를 얻는다.
중요코드 설명
std::future<int> ft = std::async(&add, 1, 2);
함수 명과 인자를 async에 넣어 호출하면 future로 return 하기에 호출 즉시 future<>로 받아야 한다.
std::launch option
std::async에 첫 번째 인자로 두 가지 옵션을 전달할 수 있는데 옵션의 기능에 대해 알아보자.
Bit | Explanation |
std::launch::async | 새로운 스레드로 실행 // enable asynchronous evaluation |
std::launch::deferred | 지연된 실행(주스레드에서 실행) // enable lazy evaluation |
* 참고 : Bit mask 타입이라 OR 조건으로도 사용이 가능하다.
예제 2
std::async의 첫 번째 인자로 launch option을 넣은 프로그램 코드이다.
#include <iostream>
#include <thread>
#include <chrono>
#include <future>
using namespace std::literals;
int add(int a, int b)
{
std::cout << "add : " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(2s); // 2초 대기
return a + b;
}
int main()
{
std::future<int> ft = std::async( std::launch::deferred, add, 10, 20);
//std::future<int> ft = std::async(add, 10, 20);
std::cout << "continue main : " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(2s); // 2초 대기
int ret = ft.get();
std::cout << "result : " << ret << std::endl;
}
결과
결과
continue main : 0x160010
add : 0x160010
result : 30
std::launch::deferred 옵션은 지연된 실행을 하는데 주 스레드에 결과가 필요할 때 호출(ft.get()하는 순간 호출) 된다. 결과를 보면 async 수행 시 새로운 스레드가 생성이 안되고 주 스레드에서 실행된 것을 확인할 수 있다.
(영상을 보면 주스레드에서 실행이 되어서 주 스레드에서 2초, add 수행 시 2초를 대기하다 보니 4초 뒤에 결과가 출력된다.)
std::launch option을 아래와 같이 제거하고 다시 실행해 보자.
As-is
std::future<int> ft = std::async( std::launch::deferred, add, 10, 20);
//std::future<int> ft = std::async(add, 10, 20);
To-be
//std::future<int> ft = std::async( std::launch::deferred, add, 10, 20);
std::future<int> ft = std::async(add, 10, 20);
결과
continue main : 0x160010
add : 0x6d2f10
result : 30
std::launch option을 생략하면 default로 동작하게 되며,
디폴트 값 : std::launch::async | std::launch::deferred (OR 동작을 함)
=> 스레드가 있는 시스템에서는 스레드로 수행
=> 스레드가 지원되지 않은 시스템에서는 지연된 실행
실행결과를 보면 이전 실행과 달리 별도 스레드에서 실행이 되었고, (체감되는지 모르겠지만) 주 스레드와 새로운 스레드에서 대기를 동시에 하기 때문에 2초 만에 결과 값이 출력된다.
추가로 알아봅시다.
ft.get()을 호출하지 않으면 어떻게 동작할까?
예제 3
std::async 수행 후 결과 값을 받는 ft.get()을 호출하지 않고 종료하는 프로그램 코드이다.
#include <iostream>
#include <thread>
#include <chrono>
#include <future>
using namespace std::literals;
int add(int a, int b)
{
std::cout << "add : " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(2s);
std::cout << "finish add" << std::endl;
return a + b;
}
int main()
{
std::future<int> ft = std::async(add, 10, 20);
std::cout << "main : " << std::this_thread::get_id() << std::endl;
//ft.get(); // ???
}
결과
main : 0x752f60
add : 0x160010
finish add
정상적으로 잘 동작한다.
결과를 보고 이상한 포인트를 찾았나요?
다음 예제를 실행해 보면 이상한 포인트를 확인할 수 있습니다.
예제 4
예제 3과 똑같이 promise/future 모델로 ft.get()을 호출하지 않고 종료하는 프로그램 코드이다.
#include <iostream>
#include <thread>
#include <chrono>
#include <future>
using namespace std::literals;
void foo(std::promise<int>&& p)
{
std::cout << "start foo" << std::endl;
std::this_thread::sleep_for(3s);
std::cout << "finish foo" << std::endl;
p.set_value(10);
}
int main()
{
std::promise<int> p;
std::future<int> ft = p.get_future();
std::thread t(foo, std::move(p));
t.detach();
// ft.get(); // ????
}
결과
_
이 코드의 실행결과는 예제 3과 달리 아무것도 출력하지 않는다. 이와 같이 주 스레드(메인 함수)가 종료되면 새로 생성된 스레드는 자동으로 종료되기에 foo 함수는 실행되지 않는다.
하지만 예제 3의 결과를 다시 보면 예제 4와 동일하게 주 스레드(메인 함수)가 종료되었음에도(?) add 함수가 정상적으로 수행이 된다.
결과
main : 0x752f60
add : 0x160010
finish add
왜 이렇게 동작을 할까? 결론부터 얘기하면 이 것은 async의 동작 특성인데 눈에 보이지 않지만 아래와 같이 동작을 하게 된다.
- main() 함수가 종료되는 순간 ft 객체는 파괴됨 => 소멸자 호출
- 소멸자에서 ft.get()으로 대기
=> 즉, 실행 중인 스레드가 종료될 때까지 대기(내부적으로 join을 호출하여 대기하고 있음)
if (_M_thread.joinable())
_M_thread.join();
다시 말해 async는 함수의 종료를 안전하게 대기하는 특성을 가지고 있다.(스레드 관점의 동작, join)
cf. 그럼 promise는?
- main() 함수가 종료되는 순간 ft 객체는 파괴됨 => 소멸자 호출
- 소멸자에서 get() 수행 안 함 => 그래서 대기 없이 종료
* 다시 말해, promise는 스레드의 관점이 아닌 p.set_value()의 결과를 얻는 것에만 관심이 있다.
async의 return 값을 받지 않으면 어떻게 동작할까?
우선을 예제를 실행해 보자.
예제 5
std::async를 수행 후 future<>를 받지 않고 종료하는 프로그램 코드이다.
#include <iostream>
#include <thread>
#include <chrono>
#include <future>
using namespace std::literals;
int add(int a, int b)
{
std::cout << "add : " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(2s);
std::cout << "finish add" << std::endl;
return a + b;
}
int main()
{
//std::future<int> ft =
std::async(add, 10, 20);
std::cout << "main : " << std::this_thread::get_id() << std::endl;
}
async는 return 값 누락 시 nodiscard 속성에 의해 return 값이 누락되어 있음을 경고로 알리게 설계되어 있다. (경고이다 보니 실행은 된다.)
async3-1.cpp:18:28: warning: ignoring return value of 'std::future<typename std::__invoke_result<typename std::decay<_Tp>::type, typename std::decay<_Args>::type ...>::type> std::async(_Fn&&, _Args&& ...) [with _Fn = int (&)(int, int); _Args = {int, int}; typename __invoke_result<typename decay<_Tp>::type, typename decay<_Args>::type ...>::type = int; typename decay<_Tp>::type = int (*)(int, int)]', declared with attribute 'nodiscard' [-Wunused-result]
18 | std::async(add, 10, 20);
| ^
In file included from D:\work\C++\concurrent\class\autoever\DAY2\02_async3-1.cpp:4:
D:/MinGW64/include/c++/13.2.0/future:1828:5: note: declared here
1828 | async(_Fn&& __fn, _Args&&... __args)
| ^~~~~
Build finished with warning(s).
결과
add : 0xc3000
finish add
main : 0x90010
이번에는 새로운 스레드가 먼저 수행되었다. 왜 이렇게 동작을 할까?
예제 3과 똑같은 관점이지만 아래와 같이 동작하기 때문이다.
async()가 반환한 것은 future<> 의 임시객체
=> 임시객체는 다음문장으로 내려가기 전에 파괴된다.
future 소멸자에서 get()이 수행되기 때문에 새로운 스레드가 종료될 때까지 (std::async(add, 10, 20) 여기서) 대기.
=> 대기가 종료되면 주 스레드의 다음 줄이 수행된다.
add함수가 별도의 스레드로 수행하긴 했지만 이처럼 주스레드에서 결과 값을 대기하면 별도스레드로 동작한 이점이 사라지기에 async 수행 시 return 값(future)을 바로 받아야 하는 이유가 여기에 있다.
참고
1. 스레드 풀 : https://change-words.tistory.com/entry/Tread-Pool
2. return이 없는 함수도 std::async를 사용할 수 있으며, return을 std::future<void>로 받으면 된다.
* 본 글에 예제 코드는 코드누리 교육을 받으면서 제공된 샘플코드를 활용하였습니다.
'L C++ > Concurrency' 카테고리의 다른 글
[Concurrency] thread_local - thread 전용 변수 ✔ (0) | 2023.12.05 |
---|---|
[Concurrency] std::jthread - join 없이 사용하기📌 (0) | 2023.11.28 |
[Concurrency] packaged_task - 기존 함수 그대로 thread에 적용(1) (0) | 2023.11.14 |
[Concurrency] Future✨에 대해 더 알아보자. (0) | 2023.11.07 |
[Concurrency] Promise /Future 모델 적용 (0) | 2023.10.31 |