Future
카테고리: Server
인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.
모두의 코드 씹어먹는 C++ 자료를 참고했습니다.
Future & Promise
C++에서는 표준 라이브러리를 통해 비동기적 실행을 간단히 실행할 수 있게 해주는 도구를 제공한다. 비동기적 실행으로 하고 싶은 작업은 어떤 데이터를 다른 쓰레드를 통해 처리한 후 결과를 받아내는 것이다. 다시 말해 “내가 어떤 쓰레드 T를 사용해 비동기적으로 값을 받아내겠다” 라는 의미는, 미래(future)에 쓰레드 T가 원하는 데이터를 돌려 주겠다라는 약속(promise)라고 볼 수 있다.
void worker(std::promise<string>* p) {
// 약속 이행. 결과는 future에 들어감
p->set_value("some data");
}
int main() {
std::promise<string> p;
// 미래에 string 데이터를 돌려 주겠다는 약속
std::future<string> data = p.get_future();
std::thread t(worker, &p);
// 약속된 데이터를 받을 때까지 대기
data.wait();
// get()안에 wait()도 포함되어 있음
cout << data.get() << endl;
t.join();
}
참고로 future 객체에서 get을 호출하면 설정된 객체가 이동된다. 따라서 get을 여러번 호출하면 오류가 발생한다. 또한 future는 예외도 전달할 수 있다.
void worker(std::promise<string>* p) {
try {
throw std::runtime_error("Some Error!");
}
catch (...) {
// set_exception 에는 exception_ptr 를 전달
p->set_exception(std::current_exception());
}
}
int main() {
std::promise<string> p;
// 미래에 string 데이터를 돌려 주겠다는 약속
std::future<string> data = p.get_future();
std::thread t(worker, &p);
// 약속된 데이터를 받을 때까지 대기
data.wait();
try {
data.get();
}
catch (const std::exception& e) {
std::cout << "예외 : " << e.what() << std::endl;
}
t.join();
}
wait_for
void worker(std::promise<void>* p) {
this_thread::sleep_for(100ms);
p->set_value();
}
int main() {
// void를 사용하면 future의 set 여부로 flag 역할을 수행할 수 있음
std::promise<void> p;
std::future<void> data = p.get_future();
thread t(worker, &p);
while (true) {
std::future_status status = data.wait_for(1ms);
// 아직 준비 안됨
if (status == std::future_status::timeout) {
continue;
}
else if (status == std::future_status::ready) {
// Do Something
break;
}
}
t.join();
}
wait_for
함수는 promise
가 설정(set_value)될 때까지 기다리는 대신에 전달된 시간 만큼만 기다리다가 바로 리턴해버린다. 이 때 리턴값은 future의 상태를 나타내는 future_status객체인데, 이를 이용해 Lock에서 구현한 내용들을 비슷하게 구현할 수 있다.
shared_future
future의 경우 get을 호출하면 내부 객체가 이동되기 때문에 get을 한 번 밖에 호출하지 못한다. 하지만 shared_future
를 사용하면 여러 쓰레드에서 future를 get 할 수 있다.
void runner(std::shared_future<void> start)
{
start.get();
cout << "출발!" << endl;
}
int main()
{
std::promise<void> p;
std::shared_future<void> start = p.get_future();
vector<thread> player;
for (int i = 0; i < 5; i++) {
player.push_back(thread(runner, start));
}
// cerr은 버퍼를 사용하지 않기 때문에 터미널에 바로 출력
std::cerr << "준비...";
this_thread::sleep_for(1ms);
std::cerr << "땅!" << endl;
p.set_value();
for (int i = 0; i < 5; i++) {
player[i].join();
}
}
위와 같이 shared_future는 일반 future와 다르게 복사가 가능하고, 복사본들이 모두 같은 객체를 공유한다. 따라서 레퍼런스나 포인터로 전달할 필요는 없다. shared_future를 잘 이용하면 condition_variable과 동일한 기능을 수행할 수 있는데, 훨씬 편리하게 구현할 수 있다. 다만, 단발성 결과가 아니라면 사용하기 어려운 것 같다.
packaged_task
packaged_task를 이용하면 기존의 Callable객체를 쉽게 wrapping하여 비동기 처리할 수 있다. packed_task에 전달된 함수가 리턴될 때, 그 리턴값을 promise에 set_value하고, 만약 예외를 던졌다면 set_exception을 하게 된다.
int some_task(int x)
{
return x + 10;
}
int main()
{
// int(int) : int를 리턴하고, 인자로 int를 받는 함수
std::packaged_task<int(int)> task(some_task);
std::future<int> start = task.get_future();
thread t(std::move(task), 5);
cout << start.get() << endl;
t.join();
}
packaged_task 역시 thread와 마찬가지로 복사 생성이 불가능하다(promise도 마찬가지). 따라서 명시적으로 move 해서 넘겨줘야 한다. packaged_task를 이용하면 쓰레드에 굳이 promise를 전달하지 않아도 되기 때문에 매우 편리하다.
async
앞서 promise나 packaged_task는 비동기 실행을 하기 위해 쓰레드를 명시적으로 생성해 실행해야만 했다. 하지만 std::async
는 어떤 함수를 전달하면 알아서 쓰레드까지 만들어 함수를 비동기적으로 실행하고, 결과값을 future에 전달한다.
int do_work(int x)
{
this_thread::sleep_for(3000ms);
return x;
}
int main()
{
auto f1 = std::async([]() {do_work(3); });
auto f2 = std::async([]() {do_work(3); });
f1.get();
f2.get();
}
async는 첫 인자로 어떤 형태로 실행할지를 선택할 수 있다
std::launch::async
: 바로 쓰레드를 생성해서 인자로 전달된 함수를 실행std::launch::deferred
: future의 get 함수가 호출되었을 때 실행 (선언할 때 쓰레드가 생성되지 않음)
이처럼 future, promise, packaged_task, async를 잘 활용하면 귀찮은 mutex나 condition_variable을 사용하지 않고도 비동기 작업을 수행할 수 있다.
댓글 남기기