CodeNote
Coding Note
CodeNote
전체 방문자
오늘
어제
  • 전체 보기 (35)
    • C++ (33)
      • Modern C++ (12)
      • Modern C++ STL (0)
      • Thread (16)
      • Thread (Async) (5)
    • 디자인패턴 (0)
    • Algorithm (2)
    • Electron (0)
    • Python (0)

블로그 메뉴

  • 홈
  • Github
  • 태그
  • 방명록

공지사항

인기 글

태그

  • LOCK
  • C++ #Memory
  • 자료구조
  • Free
  • mutex
  • C++

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
CodeNote

Coding Note

[Async] std::async
C++/Thread (Async)

[Async] std::async

2022. 8. 14. 00:24

std::async

std:::async는 C++에서 비동기호출을 매우 쉽게 만들어준다. 이전에 promise와 future를 통한 커뮤니케이션 채널을 통해 비동기 함수를 구현할 수 있었다. 하지만 그 인터페이스를 보면 코드를 만들어가는 과정이 직관적이지 않다. 그래서 이러한 관계를 추상화시켜서 비동기 함수 호출을 더 쉽게 직관적으로 만들 수 있다.

 

간단한 코드부터 보자.

Example Code : future, promise 컨셉

#include <iostream>
#include <chrono>
#include <future>
#include <thread>
#include <vector>

void add1(std::promise<int> prms, int n)
{
    prms.set_value(n+1);
}

int main()
{
    int num = 42;

    std::promise<int> prms;
    std::future<int> fut = prms.get_future();

    std::jthread t(add1, std::move(prms), num);

    int ret = fut.get();
    std::cout << "ret: " << ret << std::endl;

    return 0;
}

출력 결과

원하는 결과인 43이 나왔지만 그 구조가 직관적이지 않다.

 

Example Code : std::async 사용

#include <iostream>
#include <chrono>
#include <future>
#include <thread>
#include <vector>

int add1(int n)
{
    return n + 1;
}

int main()
{
    int num = 42;

    std::future<int> fut = std::async(add1, num);
    int ret = fut.get();
    std::cout << "ret: " << ret << std::endl;

    return 0;
}

출력 결과

std::async 함수를 사용하여 코드는 직관적이고 간결하며 결과 역시 같다.  또한, exception의 경우에도 future와 promise에서 처럼 복잡한 구조가 아니라 직관적으로 try catch 구문을 사용하면 된다.

 

Example Code : exception

#include <iostream>
#include <chrono>
#include <future>
#include <thread>
#include <vector>

int add1(int n)
{
    throw std::runtime_error("error");
    return n + 1;
}

int main()
{
    int num = 42;

    try
    {
        std::future<int> fut = std::async(std::launch::async, add1, num);
        int ret = fut.get();
        std::cout << "ret: " << ret << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "exception: " << e.what() << std::endl;
    }

    return 0;
}

출력 결과

결과를 보면 exception에 대해서도 직관적으로 try catch 구문을 사용할 수 있다.


std::async 함수에 대해서 알아야하는 점이 있다. std::async 함수는 비동기적으로 실행된다고 하였지 다른 thread에서 실행된다고 하지 않았다. std::async 함수는 launch policy를 정할 수 있는데 현재 코드에서는 따로 지정하지 않았기 때문에 컴파일러 혹은 C++ 런타임에 새로운 thread를 만들지 아니면 Main thread에서 lazy evaluation으로 실행할지 결정하는 것이다.

 

C++ 런타임이라 함은 libstdc++, libc++, msvc(STL)을 의미한다.  cppreference를보았을 때 policy를 따로 지정하지 않으면 async 혹은 deferred로 동작이 된다고 한다. 이는 라이브러리 구현에 따른다고 이해하면 된다. 이 말은 default policy를 사용하는 같은 소스 코드라하여도 플랫폼 환경에 따라 다른 동작을 할 수 있다는 말이다. 따라서, std::async를 사용할 때는 꼭 launch policy를 사용하는 것이 권장된다.

 

Launch Policy

  • async : 새로운 thread를 만들어서 fn을 실행하고 결과값은 future인터페이스의 get함수를 통해 받아온다.

  • deferred : 새로운 thread를 생성시키지 않으며 기존 thread에서 fn을 바로 실행시키지 않고 나중에 future의 get 함수를 호출하였을 때 (기존 thread에서) fn 함수를 호출하여 계산을 한 후 리턴 결과를 받는다. 즉, 필요한 순간에 실행하기 때문에 lazy evaluation이라고 하는 것이다. 최종적으로 thread는 새로 생성되지 않고 Main thread 하나만 동작하였다.

async 주의점

std::async를 그냥 사용하기에는 고려해야할 사항들이 있다. 라이브러리 구현에 따라 다르겠지만 std::async는 비싼 동작이다. 이 함수의 return 타입은 future이다. future를 통한 커뮤니케이션 채널은 내부적으로 Heap memory allocation, mutex, cv 등을 내장하고 있기 때문에 제법 비싼 동작이다. 게다가 std::async를 통해 새로운 thread를 매번 생성한다면 함수 호출시마다 Thread를 생성하고 소멸시키는 것이기 때문에 비용을 더욱 비싸게 만든다.

 

다음으로 고려해야할 사항이 launch policy인데 std::launch::async로 지정했을 때도 라이브러리 구현에 따라 다르기 때문에 조심해야한다.

  • libstdc++ : async로 실행시키는 thread를 새로운 thread를 사용하여 일을 할당한다.
  • msvc (stl) : 내부적으로 thread pool을 사용한다.

위와 같은 이유 때문에 이전에 배웠던 thread_local과 같은 키워드를 std::async와 함께 사용하게 되면 예상하지 못한 결과가 나올 수 있다.

Example Code

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

thread_local int localInt = 0;
void fn()
{
    std::cout << ++localInt << " ";
}

int main()
{
    for (int i = 0; i < 100; i++)
    {
        std::async(std::launch::async, fn);
    }
    return 0;
}
  • gcc의 libstdc++ 출력 결과

gcc 출력 결과

  • msvc stl 출력 결과

출력 결과

gcc의 경우 매번 새로운 thread를 생성하였기 때문에 1이 100번 출력된다. 반면 msvc의 경우엔 thread가 재사용되기 때문에 local thread의 값이 계속해서 증가되어 출력된다. 따라서, std::async를 사용할 때는 callable object를 Task로만 바라보고 코드를 짜는 것이 좋다.

 

future 타입의 특성

마지막으로 future 타입만의 특성이 있는데 먼저 코드를 보자.

Example Code

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

using namespace std::chrono_literals;
void fn1s()
{
    std::this_thread::sleep_for(1s);
    std::cout << "fn1s" << std::endl;
}

void fn2s()
{
    std::this_thread::sleep_for(2s);
    std::cout << "fn2s" << std::endl;
}

int main()
{
    // 1. future 타입을 받았을 때
    auto fut1 = std::async(std::launch::async, fn1s);
    auto fut2 = std::async(std::launch::async, fn2s);
    
    // 2. future 타입을 받지 않았을 때
    // ~future()
    std::async(std::launch::async, fn1s);
    // ~future()
    std::async(std::launch::async, fn2s);

    return 0;
}

[1] 주석 내용 출력 결과

처음에 Main thread가 존재하고 1초가 걸리는 task와 2초가 걸리는 task를 비동기적으로 독립적으로 실행하였기 때문에 최종적으로 전체 프로세스가 2초에 끝날 것이다. 실제로 출력 결과도 그와 같다.

 

하지만 여기서 future타입의 오브젝트를 생성하지 않고 실행시키면,

[2] 주석 내용 출력 결과

프로세스 시간이 3초가 걸렸다. 그 이유는 cppreference에서 future의 destrcutor 조건을 보면 알 수 있다.

destructor 조건

  1.  std::async를 사용하여 future가 생성되었다.
  2. 준비가 되지 않았다.
  3. 마지막 reference다.

이 조건이 충족되면서 block이 된 것이다. 즉, 눈에는 보이지 않지만 임시적인 future 타입이 생성되었다 소멸이 되면서 각각 1초, 2초간 block이 된 것이다. 이런 타입의 async 호출을 fire and forget이라고 부른다고 한다. 이렇게 임시 future의 destructor에서 block이 되는 점을 잊지 말아야한다.

'C++ > Thread (Async)' 카테고리의 다른 글

[Async] std::packaged_task  (0) 2022.08.14
[Async] shared_future  (0) 2022.08.11
[Async] Future, Promise  (0) 2022.08.10
[Async] 비동기 함수 Introduction  (0) 2022.08.10
    'C++/Thread (Async)' 카테고리의 다른 글
    • [Async] std::packaged_task
    • [Async] shared_future
    • [Async] Future, Promise
    • [Async] 비동기 함수 Introduction
    CodeNote
    CodeNote
    기록 블로그

    티스토리툴바