C++ Chapter 19.5 : 작업 기반 비동기 프로그래밍 async, future, promise
카테고리: Cpp
태그: Cpp Programming
인프런에 있는 홍정모 교수님의 홍정모의 따라 하며 배우는 C++ 강의를 듣고 정리한 필기입니다. 😀
🌜 [홍정모의 따라 하며 배우는 C++]강의 들으러 가기!
chapter 19. 모던 C++ 필수 요소들
작업 기반 비동기 프로그래밍 async, future, promise
#include <future>
Thread
vsFuture
vsPromise
- 스레드 기반의 병렬 처리와 **작업 기반의 병렬 처리**의 차이를 이해하고 비교해보자.
🔔 동기 Vs. 비동기
- 동기적 실행
- 👉 한번에 하나씩 순차적으로 실행. A 작업이 실행 중이라면 그 뒤의 작업들은 A 작업이 끝난 후에야 실행됨
- 비동기적 실행
- 👉 프로그램 실행이 여러갈래로 갈라져서 동시에 진행되는 것
동기적 실행
string txt = read("a.txt"); // 5ms
string result = do_something_with_txt(txt); // 5ms
do_other_computation(); // 5ms 걸림 (CPU 로 연산을 수행함)
- 메모리(RAM)보다 하드디스크를 읽는 시간은 8 만배나 더 느리다.
- read함수는 하드디스크를 읽기떄문에 매우 느릴 것이고
- txt를 비롯하여 밑에 result와 함수도 read가 수행을 마칠 때까지 기다려야한다.
- 이처럼 순차적으로(=동기적으로) 하나씩 실행할땐 비효율적이다.
- 상당히 오래 걸리는 파일 읽기 작업이 끝나기 전 까지 CPU는 아무것도 하지 않은 채로 기다려야 하기 때문이다.
- 위 코드는 총 5+5+5 = 15ms 걸림
- read 함수의 실행으로 하드 디스크를 읽어 오는 동안 CPU가 놀지 않고 do_other_computation() 같은 다른 일을 먼저 하고있으면 어떨까?
비동기적 실행
void file_read(string* result) {
string txt = read("a.txt"); // 1️⃣
*result = do_something_with_txt(txt);
}
int main() {
string result;
thread t(file_read, &result); // &result는 file_read 함수의 string* result 매개 변수에 대입될 인자
do_other_computation(); // 2️⃣
t.join();
}
t
스레드는 하드 디스크를 읽어오는 file_read 함수의 기능을 수행하는 역할이다.- thread t(file_read, &result)
- string txt = read(“a.txt”) 수행 👉 5ms
- *result = do_something_with_txt(txt) 수행 👉 5ms
- thread t(file_read, &result)
t
스레드에게 하드 디스크를 읽는 작업을 시켜둔 동안 Main 스레드는 다른 일을 한다.- do_other_computation()
- Main 스레드는 do_other_computation() 작업을 끝낸 후
t
스레드의 하드 디스크를 읽는 작업이 다 끝나기를 기다림- t.join()
- 총 걸린 시간 10 ms
- Main 스레드가 do_other_computation() 을 수행하는 동안
t1
스레드는 string txt = read(“a.txt”) 을 동시에 병렬적으로 수행하는데 Main 스레드가 하는 일은 하드 디스크를 읽어오는 read 함수 보다는 더 빠른 시간내에 마무리 되므로
string txt = read(“a.txt”)연산시간인 5 ms 만 걸린다고 보면 된다. - 여기에 *result = do_something_with_txt(txt) 하는데 걸리는 시간 5 ms
- Main 스레드가 do_other_computation() 을 수행하는 동안
- 이처럼 프로그램 시행이 여러갈래로 갈라져서 동시에 진행되는 것을
비동기적 실행
이라고 한다.- CPU는 한순간도 놀지 않았다.
🔔 스레드 기반 비동기적 실행 Vs. 작업 기반 비동기적 실행
둘 다 비동기적 실행이다.
std::thread
만 사용하는 스레드 기반 비동기적 실행- 👉 스레드를 통해 어떤 일을 겪게 될지 알 수 없다. 병렬 처리로 어떤 값을 받게될지 알 수 없다.
std::async
를 사용하는 작업 기반 비동기적 실행- 👉 어떤 일을 할 것인가, 미래에 어떤 값을 받아낼 것인가에 더 관심이 있다. 병렬 처리간에 값을 주고 받기가 쉽다.
- 스레드의 결과를 리턴으로 받는다.
멀티 스레딩
// multi-threading
{
int result;
std::thread t([&] {result = 1 + 2; }); // 모든 바깥 변수를 레퍼런스로 받으므로 result 값이 변화한다.
t.join();
cout << result << '\n';
}
result
은 스레드 밖에 있기 때문에result
입장에서 스레드를 통해 어떤일을 겪을지 알 수 없다.
어떤 스레드가 이 작업을 하게 될지를 중점으로 둔다.
스레드 t
의 작업으로 result
값이 3 이 됐다는 것을 알 수 없다. 만약 여러 스레드들이 병렬로 처리 될 때 스레드 t
의 작업의 결과로 result
값이 3 이 됐다는 것을 다른 스레드들이 알고 이 결과를 가지고 작업해야 하는 일이 생긴다면 차질이 생길 것이다. 모르니까! 그래서 result
를 출력하는 작업을 하기 위해선 t1
스레드가 result
을 write 하는 작업을 해야하므로 t.join()
, t1
스레드가 끝날 때 까지 기다려야 한다.
작업 기반 std::async
, std::future
// task-based parallellism
{
std::future<int> future = std::async([] {return (1 + 2);}); // auto future 쓰는게 더 간편
cout << future.get() << '\n'; // 3 출력
}
std::async
👉 처리 결과를 리턴하는 스레드라고 볼 수 있다.
std::async
의 결과가futrue
에 리턴될 때까지future.get()
작업은 실행하지 않고 기다린다.
어떤 작업(Task)을 할 것인가를 중점으로 둔다.
std::aync
- 스레드보다 선호되는 추세다.
- 처리 결과를
std::future
객체로 리턴한다.- 일반적인 return 과는 다르다
return 1 + 2
작업 앞에 더 오래 걸리는 복잡한 작업이 있다면return 1 + 2
결과가 바로future
에 저장된다는 보장이 없다.- 즉, 리턴 값은 현재 받지 못하는 값일 수도 있음을, 미래에 받을 수도 있음을 염두해두고 기다린다.
- 일반적인 return 과는 다르다
- 미래에 std::async 가 일처리를 다 끝내고 리턴값을 받을 때까지 기다렸다가 그제서야
future.get()
을 수행한다.- 람다 함수의 리턴 타입이 void 인데도 불구하고 내부에서
return 1 + 2
하는 것을 확인할 수 있다.std::aync
은 람다 함수 리턴 타입과 상관없이return
을 사용하기도 한다.
- 람다 함수의 리턴 타입이 void 인데도 불구하고 내부에서
- Main스레드가
return 1 + 2
작업은std::async
에게 병렬적으로 맡겨 놓고future.get()
을 출력하는 일을 바로 실행할 수 있음에도 불구하고return 1 + 2
작업이 끝날 때까지 자동으로 기다려 준다.join
하는거나 마찬가지!
future
변수는 정확하게async
를 통해 자신이 어떤 일을 겪게 될지를 정확하게 알 수 있다.
🔔 std::promise
✨
std::future
와std::thread
스레드를 함게 사용하려면 반드시std::future
를 받을 수 있는 존재인std::promise
가 필요하다.✨
async
가 아닌 스레드를 사용할 때, 병렬 처리를 하는 동안 미래의 데이터(future
)를 받은 후에 작업을 수행하려면 promise
를 통해 스레드가 미래에 원하는 데이터를 돌려 주겠다는 약속 해야 한다. ✨즉, 스레드의 연산 결과를 promise
을 통해 future
에 담아둔다.✨
promise
👉 생산자-소비자 패턴에서 마치 생산자 (producer) 의 역할을 수행future
👉 소비자 (consumer) 의 역할을 수행
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int main()
{
// future and promsise
{
std::promise<int> prom;
auto future = prom.get_future();
auto t = std::thread([](std::promise<int>&& prom)
{
prom.set_value(1 + 2);
}, std::move(prom));
cout << future.get() << endl;
t.join();
}
}
스레드의 결과를
future
에 담으려 할 땐,promise
에 결과를 담아야 한다. 그러려면 promise 를 R-Value Reference 으로 스레드가 수행할 함수의 인수로 넘겨주어야 한다.
prom
- std::promise<int> prom
- 안에
future
객체를 담고 있다. 미래에 결과를 이 곳에 담겠단느 약속.- 스레드는
async
와 다르게future
를 리턴하지 않기 때문에 스레드의 처리 결과를future
타입으로prom
객체에 저장할 수 있도록 코드를 짜야 한다.
- 스레드는
future
- std::future<int> future = prom.get_future()
- 미래의 연산 결과를 담을
future
객체
t
스레드- 첫 번째 인수 👉 스레드가 수행할 작업인 람다 함수.
- 두 번째 인수 👉 첫번째 인수로 넘긴 람다 함수에 필요한 인수다. std::promise<int>&& prom = std::move(prom)
- std::move(prom)
- 람다 함수에서 내부에서 R-Value Reference 타입의 매개 변수
prom
에 바깥에서 정의한prom
을 담아야 하므로 R-value로 넘겨주어야 한다. - 이처럼 스레드 내에서
promise
를 사용하려면std::thread
의 두번째 인수로promise
를 R-value 타입으로 넘겨 주어야 한다.
future.get()
- 앞서 연산하여
prom
에 담아둔prom.set_value(1 + 2)
값이future
에 남긴다. prom.set_value(1 + 2)
연산을 담당한t
스레드가 작업을 다 끝낼 때까지, 즉t
스레드의 연산 결과가 담길future
값이 담길 때까지 Main 스레드는 *cout << future.get() << endl* 을 실행하지 않고 동기적으로 기다린다.- 비동기적으로 일을 처리하되 특정 Task에 대해서는 그 결과를 사용하기 위해 Task가 완수되기를 기다리는 것이다.
t
스레드로부터 연산 결과를 받으면 그제서야 Main 스레드는 cout « future.get() « endl 을 실행한다.
- 앞서 연산하여
t.join()
- Main 스레드는
future.get()
에서t
스레드가 수행한prom
값을 이미 기다리기 때문에join
해주는게 큰 의미는 없지만 - 그래도 스레드긴 스레드이기 때문에
join()
해주는게 안정적.
- Main 스레드는
#include <iostream>
#include <future>
#include <thread>
using namespace std;
int main()
{
// future and promsise
{
std::promise<int> prom;
auto future = prom.get_future();
auto t = std::async([](std::promise<int>&& prom)
{prom.set_value(1 + 2); }, std::move(prom));
cout << future.get() << endl;
//t.join();
}
}
위 코드처럼 스레드를 async
로 대체해도 문제 없다. 다만 이런 경우엔 std::thread
가 아니므로 join
해 줄 필요가 없고, 사실 async
자체가 future
를 리턴하기 때문에 굳이 promise
를 사용할 필요가 없다. 문제 없는 코드긴 하지만 굳이..? 싶은 코드.
🔔 std::thread
Vs. std::async
비교
std::thread
- 어떤 스레드가 끝나기를 기다리려면
join
함수를 통해 대기해야 한다.
- 어떤 스레드가 끝나기를 기다리려면
std::async
- 알아서
future
객체를 리턴하므로 스레드처럼promise
를 사용할 필요가 없다. - 소멸자가 끝나는 것을 알아서 대기를 해준다.
- 따로 명시하지 않아도 자동으로
future
객체를 사용하기 전까지는 대기한다.
- 따로 명시하지 않아도 자동으로
- 주의 📢
async
는future
로 받지 않을 경우 sequential이라 생각하고 순서대로 진행이 된다.
- 알아서
async
{
auto f1 = std::async([] {
cout << "Async 1 start\n";
this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기
cout << "Async 1 end\n";
});
auto f2 = std::async([] {
cout << "Async 2 start\n";
this_thread::sleep_for(std::chrono::seconds(1)); // 1초 대기
cout << "Async 2 end\n";
});
cout << "Main function\n"; // 메인 스레드가 실행
// async는 thread와 달리 join이 없음. 내부적으로 구동 원리 차이가 있음.
}
💎출력💎
Main function
Async 1 start
Async 2 start
Async 2 end
Async 1 end
f1
,f2
는future
이다.- 비동기적 실행
- 미래의 연산 결과를
f1
에 담기로 약속이 되므로 그 전까진 비동기적으로 다른 일인f2
작업을 진행한다.- 1초만 기다리는
f2
작업이 먼저끝남
- 1초만 기다리는
- Main 스레드가 작업하는 Main 출력이 먼저 시작되는 것을 볼 수 있다.
- 미래의 연산 결과를
- 만약
f1.get()
혹은f2.get()
처럼 미래의 연산 결과를 사용해야 하는 경우에는 작업이 다 끝날때까지 기다린다.
future 를 안 받는 async 의 경우
주의 📢
async
는future
로 받지 않을 경우 sequential이라 생각하고 순서대로 진행이 된다.
{
std::async([] {
cout << "Async 1 start\n";
this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기
cout << "Async 1 end\n";
});
std::async([] {
cout << "Async 2 start\n";
this_thread::sleep_for(std::chrono::seconds(1)); // 1초 대기
cout << "Async 2 end\n";
});
cout << "Main function!\n";
}
💎출력💎
Main function
Async 1 start
Async 1 end
Async 2 start
Async 2 end
f1
,f2
에future
를 리턴하지 않는 경우- 출력 결과를 보면 그냥 순서대로, 동기적으로 실행되는 것을 볼 수 있다.
thread
{
auto f1 = std::thread([] {
cout << "Async 1 start\n";
this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기
cout << "Async 1 end\n";
});
auto f2 = std::thread([] {
cout << "Async 2 start\n";
this_thread::sleep_for(std::chrono::seconds(1)); // 1초 대기
cout << "Async 2 end\n";
});
cout << "Main function\n"; // 메인 스레드가 실행
f1.join();
f2.join();
// async는 thread와 달리 join이 없음. 내부적으로 구동 원리 차이가 있음.
}
💎출력💎
Async 1 start
Async 2 start
Main function
Async 2 end
Async 1 end
💎혹은 이렇게 출력되기도 한다💎
Async 1 start
Main function
Async 2 start
Async 2 end
Async 1 end
f1
,f2
는 스레드thread
이다.join
필수
- Main 스레드의 작업이 먼저 실행되었던
async
와 달리f1
스레드부터 먼저 시작되는 것을 알 수 있다.f1
,f2
스레드 시작 후 각자 sleep 한 후 Main 스레드가 작업하는 Main function 출력이 세번째로 됨- 이처럼 async와 thread는 내부적으로 실행순서가 다름
🔔 참고
- 모두의 코드 https://modoocode.com/284
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글남기기