Future

Date:     Updated:

카테고리:

태그:

인프런에 있는 루키스님의 게임 서버 강의를 듣고 정리한 내용입니다.
모두의 코드 씹어먹는 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();
	}
}

thread15

위와 같이 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을 사용하지 않고도 비동기 작업을 수행할 수 있다.



맨 위로 이동하기

Server 카테고리 내 다른 글 보러가기

댓글 남기기