thread_async

thread_async (비동기 작업 실행)

std::async는 함수를 비동기적으로 실행하고 그 결과를 나중에 얻을 수 있는 std::future를 반환하는 함수 템플릿이에요. 실행 정책에 따라 별도의 스레드에서 즉시 실행하거나, 결과가 요청될 때까지 지연시킬 수 있어요. 이 페이지에서는 std::async의 동작 방식과 정책, 예외 처리, 예제를 자세히 설명할게요.

출처: cppreference

본문

헤더 및 선언

<future> 헤더에 정의됨
template < class F , class ... Args > std :: future < /* see below */ > async ( F && f , Args && ... args ); (1) (C++11부터)
template < class F , class ... Args > std :: future < /* see below */ > async ( std :: launch policy , F && f , Args && ... args ); (2) (C++11부터)

함수 템플릿 std::async는 함수 f를 비동기적으로 실행하고(별도의 스레드에서 실행될 수 있으며, 이 스레드는 스레드 풀의 일부일 수도 있어요) 그 함수 호출의 결과를 최종적으로 보관하게 될 std::future를 반환해요.

std::async의 반환 타입은 std::future<V>인데, 여기서 V는 다음과 같아요:

typename std :: result_of < typename std :: decay < F >:: type ( typename std :: decay < Args >:: type ...) >:: type . (C++17까지)
std :: invoke_result_t < std :: decay_t < F > , std :: decay_t < Args > ... > . (C++17부터)

다음 조건 중 하나라도 만족하면 프로그램은 ill-formed예요:

F가 MoveConstructible이 아님. Args의 어떤 타입이 MoveConstructible이 아님. INVOKE ( decay-copy ( std :: forward < F > ( f )), decay-copy ( std :: forward < Args > ( args ))...)가 유효한 표현식이 아님. (C++20까지)
다음 중 하나라도 거짓이면 프로그램은 ill-formed예요: std :: is_constructible_v < std :: decay_t < F > , F > ( std :: is_constructible_v < std :: decay_t < Args > , Args > && ...) std :: is_invocable_v < std :: decay_t < F > , std :: decay_t < Args > ... > (C++20부터)

std::async 호출은 f 호출과 동기화되고, f의 완료는 공유 상태가 준비되기 전에 sequenced돼요.

매개변수

f - 호출할 Callable 객체
args - f에 전달할 매개변수
policy - 실행 방법을 제어하는 비트마스크 값

반환값

std::async 호출로 생성된 공유 상태를 참조하는 std::future를 반환해요.

실행 정책

비동기 호출 (Async invocation)

async 플래그가 설정된 경우, 즉 ( policy & std :: launch :: async ) != 0이면 std::async는 다음을 std::thread 객체로 표현되는 새 실행 스레드에서 호출해요:

INVOKE ( decay-copy ( std :: forward < F > ( f )), decay-copy ( std :: forward < Args > ( args ))...) (C++23까지)
std :: invoke ( auto ( std :: forward < F > ( f )), auto ( std :: forward < Args > ( args ))...) (C++23부터)
decay-copy의 호출은 현재 스레드에서 평가돼요. (C++23까지)
auto가 생성한 값은 현재 스레드에서 구체화돼요. (C++23부터)

함수 f가 값을 반환하거나 예외를 던지면, 그 값이나 예외는 std::async가 호출자에게 반환하는 std::future를 통해 접근할 수 있는 공유 상태에 저장돼요.

지연 호출 (Deferred invocation)

deferred 플래그가 설정된 경우, 즉 ( policy & std :: launch :: deferred ) != 0이면 std::async는 다음을 공유 상태에 저장해요:

decay-copy ( std :: forward < F > ( f ))decay-copy ( std :: forward < Args > ( args ))...를 공유 상태에 저장. (C++23까지)
auto ( std :: forward < F > ( f ))auto ( std :: forward < Args > ( args ))...를 공유 상태에 저장. (C++23부터)

지연 평가는 다음과 같이 수행돼요:

  • std::async가 호출자에게 반환한 std::future에 대해 시간 제한이 없는 대기 함수를 처음 호출하면, 그 대기 함수를 호출한 스레드(원래 std::async를 호출한 스레드일 필요는 없어요)에서 INVOKE ( std :: move ( g ), std :: move ( xyz ))를 평가해요. 여기서
gdecay-copy ( std :: forward < F > ( f ))의 저장된 값이고, xyzdecay-copy ( std :: forward < Args > ( args ))...의 저장된 복사본이에요. (C++23까지)
gauto ( std :: forward < F > ( f ))의 저장된 값이고, xyzauto ( std :: forward < Args > ( args ))...의 저장된 복사본이에요. (C++23부터)
  • 결과나 예외는 반환된 std::future와 연관된 공유 상태에 저장되고, 그 후에야 준비 상태가 돼요. 이후 같은 std::future에 대한 모든 접근은 즉시 결과를 반환해요.

기타 정책

policystd::launch::asyncstd::launch::deferred도, 구현 정의 정책 플래그도 설정되어 있지 않으면 동작은 정의되지 않아요.

정책 선택

둘 이상의 플래그가 설정된 경우 어떤 정책이 선택될지는 구현 정의예요. 기본값(정책에 std::launch::asyncstd::launch::deferred 플래그가 모두 설정된 경우)에 대해 표준은 사용 가능한 동시성을 활용하고 추가 작업은 지연시키는 것을 권장하지만(요구하지는 않아요) 명시하고 있어요.

std::launch::async 정책이 선택된 경우,

  • std::async 호출이 생성한 공유 상태를 공유하는 비동기 반환 객체에 대한 대기 함수 호출은 연결된 스레드가 마치 join된 것처럼 완료될 때까지 차단되거나, 아니면 시간 초과돼요.
  • 연결된 스레드의 완료는 공유 상태를 기다리는 첫 번째 함수의 성공적인 반환 또는 공유 상태를 해제하는 마지막 함수의 반환 중 먼저 도래하는 것과 동기화돼요.

예외

다음과 같은 예외를 던져요:

  • 내부 데이터 구조용 메모리를 할당할 수 없으면 std::bad_alloc
  • policy == std :: launch :: async이고 구현이 새 스레드를 시작할 수 없으면 오류 조건 std::errc::resource_unavailable_try_again을 가진 std::system_error

policystd::launch::async | std::launch::deferred이거나 추가 비트가 설정된 경우에는 이 경우 지연 호출 또는 구현 정의 정책으로 대체돼요.

참고 사항

구현은 기본 실행 정책에 추가적인(구현 정의) 비트를 활성화하여 std::async의 첫 번째 오버로드 동작을 확장할 수 있어요.

구현 정의 실행 정책의 예로는 동기 정책(std::async 호출 내에서 즉시 실행)과 작업 정책(std::async와 유사하지만 스레드 로컬 변수가 지워지지 않음)이 있어요.

std::async에서 얻은 std::future를 이동하지 않거나 참조에 바인딩하지 않으면, std::future의 소멸자는 전체 표현식의 끝에서 비동기 작업이 완료될 때까지 차단해요. 따라서 다음과 같은 코드는 본질적으로 동기적으로 동작하게 돼요:

std::async(std::launch::async, []{ f(); }); // 임시 객체의 소멸자가 f()를 기다려요
std::async(std::launch::async, []{ g(); }); // f()가 완료될 때까지 시작하지 않아요

std::async 호출 이외의 방법으로 얻은 std::future의 소멸자는 절대 차단하지 않는다는 점에 유의하세요.

예제

#include <algorithm>
#include <future>
#include <iostream>
#include <mutex>
#include <numeric>
#include <string>
#include <vector>

std::mutex m;

struct X
{
    void foo(int i, const std::string& str)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << str << ' ' << i << '\n';
    }
    
    void bar(const std::string& str)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << str << '\n';
    }
    
    int operator()(int i)
    {
        std::lock_guard<std::mutex> lk(m);
        std::cout << i << '\n';
        return i + 10;
    }
};

template<typename RandomIt>
int parallel_sum(RandomIt beg, RandomIt end)
{
    auto len = end - beg;
    if (len < 1000)
        return std::accumulate(beg, end, 0);
    
    RandomIt mid = beg + len / 2;
    auto handle = std::async(std::launch::async,
                             parallel_sum<RandomIt>, mid, end);
    int sum = parallel_sum(beg, mid);
    return sum + handle.get();
}

int main()
{
    std::vector<int> v(10000, 1);
    std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n';
    
    X x;
    // 기본 정책으로 (&x)->foo(42, "Hello")를 호출해요:
    // "Hello 42"를 동시에 출력하거나 실행을 지연할 수 있어요
    auto a1 = std::async(&X::foo, &x, 42, "Hello");
    // 지연 정책으로 x.bar("world!")를 호출해요
    // a2.get() 또는 a2.wait()가 호출될 때 "world!"를 출력해요
    auto a2 = std::async(std::launch::deferred, &X::bar, x, "world!");
    // async 정책으로 X()(43)을 호출해요
    // "43"을 동시에 출력해요
    auto a3 = std::async(std::launch::async, X(), 43);
    a2.wait();                     // "world!"를 출력해요
    std::cout << a3.get() << '\n'; // "53"을 출력해요
} // a1이 이 시점에 완료되지 않았다면, a1의 소멸자가 여기서 "Hello 42"를 출력해요

가능한 출력:

The sum is 10000
43
world!
53
Hello 42

결함 보고서

다음 동작 변경 결함 보고서는 이전에 발표된 C++ 표준에 소급 적용되었어요.

DR 적용 대상 발표된 동작 올바른 동작
LWG 2021 C++11 반환 타입이 부정확하고 지연 경우의 인수 값 범주가 불명확함 반환 타입을 수정하고 rvalue가 사용됨을 명확히 함
LWG 2078 C++11 policystd::launch::async 외의 다른 실행 정책을 지정할 때 std::system_error가 발생할 수 있는지 불명확했음 policy == std :: launch :: async인 경우에만 발생할 수 있음
LWG 2100 C++11 std::launch::async 정책을 사용할 때 시간 제한 대기 함수가 시간 초과할 수 없었음 허용함
LWG 2120 C++11 표준 또는 구현 정의 정책이 설정되지 않은 경우 동작이 불명확했음 이 경우 동작은 정의되지 않음
LWG 2186 C++11 지연 평가에서 반환된 값과 던져진 예외가 어떻게 처리되는지 불명확했음 공유 상태에 저장됨
LWG 2752 C++11 내부 데이터 구조용 메모리를 할당할 수 없을 때 std::asyncstd::bad_alloc을 던지지 않을 수 있었음 던짐
LWG 3476 C++20 (decay된 타입의) F와 인수 타입이 직접 move constructible이어야 했음 이 요구 사항을 제거함 [ 1 ]
  • ↑ move-constructibility는 std::is_constructible_v에 의해 이미 간접적으로 요구돼요.

같이 보기

future (C++11) 비동기적으로 설정되는 값을 기다리는 클래스 템플릿 [편집]
C++ 문서: 실행 지원 라이브러리

더 알아보기 (Learn more)

cppreference