전 게시글의 조건변수는 생산자, 소비자와 같이 한 스레드에서는 데이터를 밀어넣고 다른 스레드에서는 데이터를 꺼내 쓰는 방식에서 유용하게 작동한다는 것을 알 수 있었다. 하지만 단발성으로 이벤트를 보내줘야할 상황이 생길 수 있는데 이럴 때 조건변수까지 사용하지 않고 더 가볍게 처리할 수 있는 방법이 있다. 그것이 Future, 미래 객체이다.
#include <iostream>
int Calculate()
{
int sum = 0;
for (int i = 0; i < 10000; i++)
{
sum += i;
}
return sum;
}
int main()
{
// 동기 실행
int sum = Calculate();
cout << sum << endl;
}
10000미만의 자연수를 더하는 단순한 함수가 있다. 실행 순서를 보게되면 Calculate()함수가 호출 될 때 Calculate() 내부로 들어가서 코드를 실행하고 sum을 반환하면서 다시 main으로 돌아와 출력하게 될 것이다. 즉 함수가 호출 될 때 main은 멈춰있는 것이다. 이러한 실행 방식을 동기 실행이라고 한다. 동기 실행은 작업이 끝나기 전까지 기다리는 것으로 만약 Calculate()라는 함수가 몇 분씩 실행된다고 하면 main함수는 그 시간동안 아무런 작업을 하지 못하고 기다려야 하는 일이 발생하게 된다. 그럴 경우 상황에 따라 비동기 실행이 더 유용하게 작동하는 경우가 있다.
#include <iostream>
#include <thread>
int result;
int Calculate()
{
int sum = 0;
for (int i = 0; i < 10000; i++)
{
sum += i;
}
result = sum;
return sum;
}
int main()
{
// 동기 실행
//int sum = Calculate();
thread t(Calculate);
t.join();
cout << result << endl;
}
이렇게 스레드를 이용해 작업을 처리하는 것이 비동기의 대표적인 예시이다. 하지만 데이터를 넘겨받기 위해서 공유 데이터를 사용해야한다는 점과 다중 스레드일 경우 여러개의 공유 데이터를 사용하거나 락을 통해 공유 데이터를 보호해야 한다는 점이 번거롭게 느껴진다. 또한 단순한 작업일 뿐인데 그것을 위해 스레드를 생성하고 소멸시키는 이러한 일들이 불필요하게 느껴지기도 한다.
이를 해결하기 위한 방법이 Future 객체인 것이다.
#include <iostream>
#include <future>
int Calculate()
{
int sum = 0;
for (int i = 0; i < 10000; i++)
{
sum += i;
}
return sum;
}
int main()
{
int sum;
// deferred -> 지연시켜서 실행
// async -> 별도의 스레드 생성하여 실행
future<int> future = async(launch::async, Calculate);
sum = future.get();
cout << sum << endl;
}
future 객체를 사용한 모습이다. async함수의 첫 번째 인자로 deferred와 async를 사용한다.
deferred의 경우 지금 당장 함수를 호출하는 것이 아니라 나중에 필요할 때 호출하여 사용하겠다는 것이다. 요청한 시점과 호출하는 시점이 다르다는 뜻이다. 그렇다면 그냥 나중에 함수를 호출하여 사용하면 되지 않느냐라고 생각할 수 있다. 하지만 서버가 어떠한 작업 요청을 받았을 때 너무 바쁜 상태여서 당장 수행할 수가 없다면 future 객체를 만들어 나중에 실행하기 위해 deferred를 사용할 수 있다. deferred는 스레드를 새로 생성하지는 않는다.
async는 별도의 스레드를 생성하여 작업을 넘겨주는 것으로 별개의 스레드가 작업을 처리하기 때문에 main 함수는 멈추지 않고 정상적으로 동작한다. 요청, 호출의 시점이 같다고 볼 수 있다.
이렇게 별도로 처리된 데이터를 필요로하여 받아오고 싶을 땐 get() 함수를 사용하여 받아올 수 있다.
근데 여기서 future가 일을 다 끝냈는지 get()을 하기 전에 알고 싶다면 어떻게 할까?
#include <iostream>
#include <future>
int Calculate()
{
int sum = 0;
for (int i = 0; i < 10000; i++)
{
sum += i;
}
return sum;
}
int main()
{
int sum;
// deferred -> 지연시켜서 실행
// async -> 별도의 스레드 생성하여 실행
future<int> future = async(launch::async, Calculate);
future_status status = future.wait_for(1ms);
if(status == future_status::ready)
{
sum = future.get();
}
cout << sum << endl;
}
wait_for()이라는 함수는 future_status라는 것을 반환하는데 이를 통해 현재 작업의 상태를 확인할 수 있다.
전역함수가 아니라 어떤 객체의 맴버함수를 호출할 경우 사용방법이 약간 달라진다.
#include <iostream>
#include <future>
class Player
{
public:
int GetHp() { return 1000; }
};
int main()
{
Player player;
future<int> future = async(launch::async, &Player::GetHp, player);
}
이런식으로 클래스의 함수를 건내주고 해당 함수를 호출할 객체도 함께 건내주어야 한다.
다음은 promise이다. promise는 말 그대로 나중에 데이터를 반환해줄 것이라 약속하는 것이다.
#include <iostream>
#include <thread>
#include <future>
void PromiseWorker(promise<string>&& promise)
{
promise.set_value("Set String");
}
int main()
{
{
promise<string> promise;
future<string> future = promise.get_future();
thread t(PromiseWorker, move(promise));
string str = future.get();
cout << str << endl;
t.join();
}
}
future 객체는 내가 가지고 있고 promise 객체는 PromiseWorker라는 함수와 함께 스레드에게 넘겼다. future 객체는 promise 객체로부터 get_future()를 통해 데이터를 받아올 수 있다. 이후 get()을 통해 데이터를 확인해 보면
위와 같이 제대로 출력되는 것을 볼 수 있다.
두 번째 future사용 방법을 알아보았다. 첫 번째의 방법은 함수를 만들어 비동기로 실행하여 결과를 받아오는 것이었고, 두 번째의 방식은 future를 세팅할 수 있는 promise라는 다른 객체를 만들어 다른 스레드에 소유권을 넘겨줌으로써 처리를 맡긴 후 future 객체로 받아오는 것이었다.
마지막 방식은 2번째 방식과 유사한 방식으로 packaged_task라는 것이다.
#include <iostream>
#include <thread>
#include <future>
int Calculate()
{
int sum = 0;
for (int i = 0; i < 10000; i++)
{
sum += i;
}
return sum;
}
void TaskWorker(packaged_task<int(void)> && task)
{
task();
}
int main()
{
//packaged_task
{
packaged_task<int(void)> task(Calculate);
future<int> future = task.get_future();
thread t(TaskWorker, move(task));
int sum = future.get();
cout << sum << endl;
t.join();
}
}
packaged_task는 선언할 때 반환값이 아니라 task에서 사용할 함수의 반환값과 파라미터를 넣어주어야 한다. Calculate 함수의 반환값이 int이고 파라미터가 없으니 int(void)를 넣어준 것이다.
TaskWorker를 보면 promise와는 달리 task는 함수처럼 바로 실행할 수 있고 promise에서 set_value를 한 것과는 다르게 반환값이 바로 future 객체로 들어가게 된다.
정상적으로 작동하는 모습이다.
정리하자면
1) async: 원하는 함수를 비동기적으로 실행
2) promise: 결과물을 promise를 통해서 future로 받음
3) packaged_task: 원하는 함수의 실행 결과를 packaged_task를 통해 바로 future로 받음
mutex, condition_variable까지 사용하지 않고 단순한 단발성 이벤트와 같은 것들을 처리하기 위해 위의 방법을 사용할 수 있다.
'서버 > 멀티스레드' 카테고리의 다른 글
11. Thread Local Storage (3) | 2024.07.25 |
---|---|
10. 메모리 모델 (0) | 2024.07.24 |
8. Condition Variable (0) | 2024.07.20 |
7. Event (0) | 2024.07.20 |
6. Sleep (0) | 2024.07.17 |