icon안동민 개발노트

비동기 프로그래밍 (async, future)


비동기 프로그래밍의 개념

 비동기 프로그래밍은 프로그램의 주 실행 흐름을 차단하지 않고 작업을 수행할 수 있게 해주는 프로그래밍 패러다임입니다. C++ 11부터 도입된 <future> 헤더의 기능들을 통해 비동기 프로그래밍을 쉽게 구현할 수 있습니다.

std::future와 std::promise

 std::future는 비동기 작업의 결과를 나타내는 객체입니다. std::promise는 이 결과를 설정하는 역할을 합니다.

#include <iostream>
#include <future>
#include <thread>
 
void producer(std::promise<int>&& intPromise, int value) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    intPromise.set_value(value);
}
 
int main() {
    std::promise<int> intPromise;
    std::future<int> intFuture = intPromise.get_future();
 
    std::thread t(producer, std::move(intPromise), 42);
 
    std::cout << "Waiting..." << std::endl;
    std::cout << "Result: " << intFuture.get() << std::endl;
 
    t.join();
    return 0;
}

std::async

 std::async는 비동기 작업을 더 쉽게 시작할 수 있게 해주는 함수입니다.

#include <iostream>
#include <future>
#include <thread>
 
int compute() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 42;
}
 
int main() {
    std::cout << "Starting computation..." << std::endl;
    std::future<int> result = std::async(std::launch::async, compute);
    
    std::cout << "Doing other work..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    std::cout << "Result: " << result.get() << std::endl;
    return 0;
}

실행 정책

 std::async는 다음과 같은 실행 정책을 지원합니다.

  • std::launch::async : 새로운 스레드에서 비동기적으로 실행
  • std::launch::deferred : 결과가 필요할 때까지 실행을 지연
  • std::launch::async | std::launch::deferred : 구현에 따라 선택
auto f1 = std::async(std::launch::async, compute);
auto f2 = std::async(std::launch::deferred, compute);

std::packaged_task

 std::packaged_task는 함수나 콜러블 객체를 future와 연결합니다.

#include <iostream>
#include <future>
#include <thread>
 
int multiply(int a, int b) {
    return a * b;
}
 
int main() {
    std::packaged_task<int(int,int)> task(multiply);
    std::future<int> result = task.get_future();
 
    std::thread t(std::move(task), 2, 3);
 
    std::cout << "Result: " << result.get() << std::endl;
 
    t.join();
    return 0;
}

예외 처리

 비동기 작업에서 발생한 예외는 future를 통해 전파됩니다.

#include <iostream>
#include <future>
#include <stdexcept>
 
void may_throw() {
    throw std::runtime_error("Error in async operation");
}
 
int main() {
    auto f = std::async(std::launch::async, may_throw);
 
    try {
        f.get();
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
 
    return 0;
}

여러 future 대기

 std::futurewait_forwait_until 메서드를 사용하여 여러 비동기 작업을 동시에 대기할 수 있습니다.

#include <iostream>
#include <future>
#include <vector>
#include <chrono>
 
int work(int id) {
    std::this_thread::sleep_for(std::chrono::seconds(id));
    return id;
}
 
int main() {
    std::vector<std::future<int>> futures;
 
    for (int i = 1; i <= 5; ++i) {
        futures.push_back(std::async(std::launch::async, work, i));
    }
 
    bool all_done = false;
    while (!all_done) {
        all_done = true;
        for (auto& f : futures) {
            if (f.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
                all_done = false;
                break;
            }
        }
        std::cout << "Waiting..." << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
 
    for (auto& f : futures) {
        std::cout << "Result: " << f.get() << std::endl;
    }
 
    return 0;
}

연습 문제

  1. std::async를 사용하여 피보나치 수열의 n번째 항을 계산하는 프로그램을 작성하세요. 여러 개의 비동기 작업을 생성하여 계산을 병렬화하세요.
  2. std::promisestd::future를 사용하여 생산자-소비자 패턴을 구현하세요. 생산자는 데이터를 생성하고, 소비자는 이를 처리합니다.
  3. 비동기 작업 체인을 구현하세요. 첫 번째 작업의 결과를 입력으로 받아 두 번째 작업을 수행하고, 그 결과를 다시 세 번째 작업의 입력으로 사용하는 방식으로 연결된 비동기 작업들을 만드세요.


참고 자료

  • C++ Concurrency in Action (2nd Edition) by Anthony Williams
  • Effective Modern C++ by Scott Meyers (특히 Item 35-40)
  • C++ 표준 문서의 <future> 헤더 관련 섹션
  • "C++ Concurrency in Action" by Anthony Williams
  • CppCon 발표 영상들 - 비동기 프로그래밍 관련 세션들
  • "Concurrency with Modern C++" by Rainer Grimm