티스토리 뷰

C・C++/멀티스레드

[C++] Future

jiggyjiggy 2024. 10. 4. 18:25

이전글 [C++] 스레드 경쟁: 스핀락, 슬립락 구현 / 이벤트, condition_variable (https://jiggyjiggy.tistory.com/137)에서, 스레드의 순서를 맞춰주기 위해 queue와 condition variable를 사용했다.

하지만 그보다 가벼운 상황일때도 존재한다. produce-consume 상황이 아닌, 단발성 이벤트의 경우가 그러하다. 그럴때, future 객체를 적용하면 좋다. (condition variable 보다 가볍고, C++ 11 표준 스펙)

상황

#include <iostream>

int Calculate()
{
	int sum = 0;

	for (int i = 0; i < 100'000; ++i)
	{
		sum += i;
	}

	return sum;
}

int main()
{
	int sum = Calculate();
	std::cout << sum << std::endl;

	return 0;
}

Calculate 가 오래걸리므로, 단기알바(스레드)를 주자

int main()
{
	int sum = Calculate();
	std::cout << sum << std::endl;

	std::thread t(Calculate);

	t.join();

	return 0;
}

그러면, 스레드에서 연산이 완료되면, 그 값을 받아와야 하는데,,

  • out 전용 변수(전역변수)로 받아올까?
    • 언제 완료되는지
    • 여러개가 있다면?
    • 일이 복잡해진다…
  • 스레드를 직접 만드는게 맞을까?
    • 잠시 사용할, 단기알바를 고용하자.
    • 스레드보다 더 가벼운 std::future !
      • 하나의 함수만 잠시 비동기로 실행하고싶을때 매우 유용 !
  • future 객체는 3가지 방법으로 만들고 사용한다.

future

int main()
{
	{
		std::future<int> future = std::async(std::launch::async, Calculate);

		// TODO

		int sum = future.get(); // 결과물이 이제서야 필요하다
	}

	return 0;
}
  • async 메서드
    • 1번째 인자: 정책 3가지
      • deferred: lazy evaluation (지연해서 실행하세요)
      • async : 별도의 스레드를 만들어서 실행하세요
      • deferred | async : 둘 중 알아서 골라주세요

일감이 진짜 끝났는지 peeking 하고 싶을 수 있다. 이런 경우 wait 계열 함수가 존재한다

int main()
{
	{
		std::future<int> future = std::async(std::launch::async, Calculate);

		// TODO
		
		std::future_status status = future.wait_for(std::chrono::milliseconds(1));
		if (status == std::future_status::ready)
		{
		}
		
		future.wait(); // == wait_for(INFINITY) == 결과물이 이 시점에 필요하다

		int sum = future.get(); // 결과물이 이제서야 필요하다
	}

	return 0;
}

📝 요약해보면, future 객체는, 언젠가 미래에 결과물을 뱉는다는 약속을 받고있는 것이다

cf) 메서드를 future 객체에 등록하려면?

  • 추가적인 조건이 필요하다 (등록할 객체를 넣어줘야함)
class Knight
{
public:
	int GetHp() { return 100; }
};

Knight knight;
std::future<int> future2 = std::async(std::launch::async, Knight::GetHp, knight);

promise

out 전용 변수로 구현해볼까? 근데 지저분하게는 싫어~

promise가 그 해결책이다!

다른 스레드에서 데이터를 밀어넣는 작업만 추가하고싶다고 가정해보자

// 미래(std::future)에 결과물을 반환해줄꺼라 약속(std::promise)해줘. (aka. 계약서)
std::promise<std::string> promise;
std::future<std::string> future = promise.get_future();
  • promise는 다른 스레드에게 넘기고
  • future는 내가 갖고 있고
void PromiseWorker(std::promise<std::string>&& promise)
{
	promise.set_value("Secret Message");
}

int main()
{
	// std::promise
	{
		// 미래(std::future)에 결과물을 반환해줄꺼라 약속(std::promise)해줘. (aka. 계약서)
		std::promise<std::string> promise;
		std::future<std::string> future = promise.get_future();

		std::thread t(PromiseWorker, std::move(promise));
		
		t.join();
	}
	
	return 0;
}
  • future는 main 스레드가 갖고있는다
  • promise의 소유권을, t 스레드에게 넘긴다
    • t 스레드가, promise.set_value("Secret Message") 를 통해 데이터를 입력하게 되면,
    • main 스레드에서 인지하고, future.get()을 통해서 받아 올 수 있게된다
void PromiseWorker(std::promise<std::string>&& promise)
{
	promise.set_value("Secret Message");
}

int main()
{
	// std::promise
	{
		// 미래(std::future)에 결과물을 반환해줄꺼라 약속(std::promise)해줘. (aka. 계약서)
		std::promise<std::string> promise;
		std::future<std::string> future = promise.get_future();

		std::thread t(PromiseWorker, std::move(promise));
		
		std::string message = future.get();
		std::cout << message << std::endl;
		
		t.join();
	}
	
	return 0;
}

정리하면, promise에다 데이터를 넣어주고, future 객체를 통해서 받는 형태로 동작한다

packaged_task

Calculate 함수를 task로 등록하려면,

int Calculate()

인자는 void , 반환형은 int 이므로

// std::packaged_task
{
	std::packaged_task<int(void)> task(Calculate);
	std::future<int> future = task.get_future();
}

cf) promise와 packaged_task는 거의 비슷하다

약간의 차이를 설명하면,

  • promise
    • 다른 스레드에서, std::promisestd::string 에 맞는 타입의 데이터를 넣어주세요
  • packaged_task
    • 다른 스레드에서, std::packaged_task<int(void)> 라는 함수(Calculate)를 호출해주세요

이다

void TaskWorker(std::packaged_task<int(void)>&& task) // 소유권을 넘겨줄 것이기에 r-value
{
	task();
}

int main()
{
	// std::packaged_task
	{
		std::packaged_task<int(void)> task(Calculate);
		std::future<int> future = task.get_future();

		std::thread t(TaskWorker, std::move(task));

		t.join();
	}

	return 0;
}
  • TaskWorker에서 특별히 하는 일은, “함수호출”이다
  • 특이한 점은, future를 통해서 받아올 수 있다는 것이다

전체코드

#include <iostream>
#include <thread>
#include <future>

int Calculate()
{
	int sum = 0;

	for (int i = 0; i < 100'000; ++i)
	{
		sum += i;
	}

	return sum;
}

void PromiseWorker(std::promise<std::string> &&promise)
{
	promise.set_value("Secret Message");
}

void TaskWorker(std::packaged_task<int(void)> &&task)
{
	task();
}

int main()
{
	// std::future
	{
		std::future<int> future = std::async(std::launch::async, Calculate);

		// TODO

		std::future_status status = future.wait_for(std::chrono::milliseconds(1));
		if (status == std::future_status::ready)
		{
		}

		future.wait(); // == wait_for(INFINITY) == 결과물이 이 시점에 필요하다

		int sum = future.get(); // 결과물이 이제서야 필요하다

		// class Knight
		// {
		// public:
		// 	int GetHp() { return 100; }
		// };
		// Knight knight;
		// std::future<int> future2 = std::async(std::launch::async, Knight::GetHp, knight);
	}

	// std::promise
	{
		// 미래(std::future)에 결과물을 반환해줄꺼라 약속(std::promise)해줘. (aka. 계약서)
		std::promise<std::string> promise;
		std::future<std::string> future = promise.get_future();

		std::thread t(PromiseWorker, std::move(promise));

		std::string message = future.get();
		std::cout << message << std::endl;

		t.join();
	}

	// std::packaged_task
	{
		std::packaged_task<int(void)> task(Calculate);
		std::future<int> future = task.get_future();

		std::thread t(TaskWorker, std::move(task));

		int sum = future.get();
		std::cout << sum << std::endl;

		t.join();
	}

	return 0;
}

결론

  • mutex, condition_variable 까지 가지 않고, 단순한 애들을 처리할 수 있는 방법을 알아보았다
  • 특히, 1회성으로 일어나는 이벤트에대해서 매우 유용하다. producer-consumer 패턴으로서 큐에 데이터를 넣고 빼는 무한적인 작업이 아니라.
  • future를 만든 각 방법의 미묘한 차이를 간단하게 요약해보자
    • future
      • 스레드를 만들어서, Calculate를 시켰다
      • 태스크 하나를 위한 전용 스레드를 만들었다
      • 원하는 함수를 비동기적으로 실행
    • promise
      • 결과물을 promise를 통해서 future로 받아줌
    • packaged_task
      • 스레드를 만든 후, 태스크를 여러개 만들어서 넘길수도 있다
      • TaskWorker에서 인자를 수정해서 여러개 받게 만들수 있을 것이다
      • 원하는 함수의 실행 결과를 packaged_task를 통해 future로 받아줌

닭잡는데 소잡는 칼을 쓸 필요 없다!

cf)

비동기는 대부분 멀티스레드로 구현하겠지만,

비동기가 반드시 멀티스레드로서 구현되지는 않는다

std::async(std::launch::deferred, Calculate);

int sum = future.get();

라면, 실행시점에서 호출되는 것이다

std::async(std::launch::deferred, Calculate);

int sum = future.Calculate();
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함