Cute Light Pink Flying Butterfly C++ 비동기 std::async만 쓰시나요? :: Taskflow와 TBB 라이브러리로 쉽게 비동기 흐름 컨트롤하기 :: 놀면서 돈벌기
본문 바로가기
IT/Architecture

C++ 비동기 std::async만 쓰시나요? :: Taskflow와 TBB 라이브러리로 쉽게 비동기 흐름 컨트롤하기

by esclife_ 2026. 2. 25.
반응형

비동기 호출의 함정, 그리고 '흐름'의 부재

 

C++에서 비동기 작업을 구현하는 가장 기본적인 방법은 std::async나 std::thread를 사용하는 것입니다. 

하지만, 동기시스템을 비동기로 수정하기위해서, 여러 개의 비동기 메서드 작업들이 얽히기 시작하면

코드는 금세 콜백 지옥(Callback Hell)이나, future 객체들의 복잡한 조합으로 뒤엉키게 됩니다.

 


(여기서 콜백지옥이란...)

비동기 작업의 '완료' 시점에 실행될 함수(콜백 함수)를 연쇄적으로 중첩(nesting)하여

코드의 가독성이 급격히 떨어지는 현상을 말합니다.
코드가 마치 오른쪽으로 계속 파고드는 피라미드(Pyramid of Doom) 모양처럼 보이게 됩니다.
[JavaScript에서의 전형적인 콜백 지옥 예시]

// Step 1: 사용자 정보를 가져온다.
getUser(userId, function(user) {
    // Step 2: 사용자 정보로 주문 목록을 가져온다.
    getOrders(user.id, function(orders) {
        // Step 3: 주문 목록 중 첫 번째 주문의 상세 정보를 가져온다.
        getOrderDetails(orders[0].id, function(details) {
            // Step 4: 상세 정보를 기반으로 배송 상태를 업데이트한다.
            updateShipping(details, function(shippingInfo) {
                // Step 5: 최종적으로 UI를 업데이트한다.
                updateUI(shippingInfo);
            });
        });
    });
});

 


 

아래 코드는 단 3개의 작업만으로도 복잡합니다. 만약 10개의 작업이 얽히고, 특정 조건에 따라 다른 작업을 실행해야 한다면 어떻게 될까요? 전체 작업의 흐름을 한눈에 파악하기 어렵고, 관리하기는 더더욱 힘들어집니다.

// 전통적인 방식: 개별 비동기 호출의 나열
auto futureA = std::async(taskA);
auto futureB = std::async(taskB);

auto resultA = futureA.get(); // A가 끝날 때까지 대기
auto resultB = futureB.get(); // B가 끝날 때까지 대기

auto futureC = std::async(taskC, resultA, resultB); // A와 B의 결과로 C 실행
auto resultC = futureC.get();


이것이 바로 "비동기를 하나하나 구현하는" 방식의 한계입니다.

Taskflow와 TBB는 이 문제를 뭉텅이로, 즉, 전체 작업 흐름을 하나의 단위로 정의하여 해결할 수 있게 해줍니다.

 


1. Taskflow: '작업 그래프'로 비동기 흐름을 그리다

Taskflow의 핵심 아이디어는 코드로 순서도를 그린다는 것입니다. 각 비동기 작업을 노드로 정의하고, 이 노드들 간의 실행 순서를 엣지로 연결하여 하나의 거대한 작업 그래프(Task Graph)를 만들어냅니다.

[이론적 예시]
"A: 이미지 로드 -> (B: 이미지 리사이즈, C: 워터마크 추가) -> D: 이미지 저장"

  • A 작업이 끝나야, B와 C 작업을 동시에(병렬로) 시작할 수 있습니다.
  • B와 C 작업이 모두 끝나야, D 작업을 시작할 수 있습니다.

 

[std::async 방식의 불편]

auto futureA = std::async(loadImage);
Image image = futureA.get();

// B와 C를 동시에 실행하고, 둘 다 끝나길 기다려야 함
auto futureB = std::async(resizeImage, image);
auto futureC = std::async(addWatermark, image);

Image resized = futureB.get();
Image watermarked = futureC.get();
//... 여기서 두 이미지를 어떻게 합칠지? 로직이 복잡해짐

saveImage(finalImage);

위 코드는 future 객체를 수동으로 관리해야 하며, B와 C의 결과를 D에 전달하는 로직이 깔끔하지 않습니다.

[Taskflow 방식의 편리함]

 

#include <taskflow/taskflow.hpp>

// 1. Taskflow와 Executor 객체 생성
tf::Executor executor;
tf::Taskflow taskflow;

// 2. '뭉텅이'로 작업 흐름 정의
auto [taskA, taskB, taskC, taskD] = taskflow.emplace(
    []() { /* A: 이미지 로드 */ },
    []() { /* B: 이미지 리사이즈 */ },
    []() { /* C: 워터마크 추가 */ },
    []() { /* D: 이미지 저장 */ }
);

// 3. '큰 기준'으로 관계 설정
taskA.precede(taskB, taskC); // A는 B와 C보다 선행한다
taskD.succeed(taskB, taskC); // D는 B와 C가 모두 끝나야 시작한다

// 4. 전체 작업 흐름을 '한 번에' 실행
executor.run(taskflow).wait();

핵심: Taskflow는 무엇을 할지(람다 함수)와 어떤 순서로 할지(precede, succeed)를 명확히 분리하여 정의합니다.

개발자는 개별 스레드나 future를 신경 쓸 필요 없이, 전체 작업의 흐름만 설계하면 됩니다. Executor가 알아서 최적의 스레드에 작업을 분배하고, 정의된 순서에 따라 비동기적으로 실행해 줍니다. 이것이 바로 "한 번에 뭉텅이로 큰 기준으로 연결해서 비동기를 설정"하는 방식의 실체입니다.

 


2. TBB: '알고리즘'으로 데이터 덩어리를 병렬 처리하다

TBB는 Taskflow처럼 복잡한 의존성 그래프를 그리는 데 특화되지는 않았지만, 거대한 데이터 덩어리를 병렬로 처리하는 시나리오에서 "뭉텅이" 접근 방식을 제공합니다.


[이론적 예시]
"100만 개의 픽셀로 이루어진 이미지의 모든 픽셀 밝기를 10% 증가시킨다."

[std::thread 방식의 고통]
100만 개의 픽셀을 8개의 스레드에 수동으로 분배하고, 각 스레드가 처리할 픽셀 범위를 직접 계산해야 합니다. 코드가 장황해지고 오류가 발생하기 쉽습니다.

[TBB 방식의 간결함]

#include <tbb/parallel_for.h>
#include <vector>

std::vector<Pixel> pixels(1000000);

// 'tbb::parallel_for' 라는 알고리즘으로 '뭉텅이' 처리
tbb::parallel_for(tbb::blocked_range<int>(0, 1000000), 
    [&](const tbb::blocked_range<int>& range) {
        for (int i = range.begin(); i != range.end(); ++i) {
            pixels[i].brightness *= 1.10;
        }
    }
);

핵심: 개발자는 parallel_for라는 병렬 처리 알고리즘에게 "0부터 100만까지의 범위를 알아서 나눠서 이 작업을 수행해 줘"라고 명령하기만 하면 됩니다. TBB의 런타임 스케줄러가 현재 시스템의 코어 수에 맞게 범위를 자동으로 분할(blocked_range)하고, 각 스레드에 작업을 할당하여 최적으로 실행합니다. 여기서도 개발자는 개별 스레드를 전혀 다루지 않습니다.


 


'어떻게'가 아닌 '무엇을'에 집중하라

 

비동기 구현을 "한 번에 뭉텅이로, 큰 기준으로" 연결하고 설정한다는 것은,

저수준의 스레드 제어(직접 하나하나 코딩)에서 벗어나 작업의 논리적 흐름이나 병렬 처리할 데이터 범위라는

더 높은 추상화 수준에서 프로그래밍하는 것을 의미합니다.

* Taskflow는 복잡한 작업 의존성을 '그래프'라는 뭉텅이로 정의하여 비동기 흐름을 제어합니다.

* TBB는 거대한 데이터를 '병렬 알고리즘'이라는 뭉텅이로 정의하여 처리합니다.

이 라이브러리들을 활용하면, 우리는 비동기 작업 하나하나의 구현이 아닌, 전체 시스템의 흐름과 구조를 설계하는 데

집중할 수 있게 됩니다. 이것이 바로 현대 C++ 병렬 프로그래밍의 핵심입니다.

반응형