Boost.Asio에서 비동기 프로그래밍의 핵심 개념 중 하나는 I/O 객체와 핸들러의 역할이다. I/O 객체는 시스템 자원과 상호작용하기 위한 인터페이스를 제공하며, 비동기 작업의 시작점이 된다. 반면, 핸들러는 비동기 작업이 완료될 때 호출되는 콜백 함수로, 주로 오류 처리와 결과 처리를 담당한다.

I/O 객체의 역할

I/O 객체는 주로 네트워크 소켓, 파일 디스크립터 또는 타이머와 같은 자원을 관리하며, 이를 통해 데이터 송수신, 파일 입출력, 시간 기반 작업 등을 처리한다. 이러한 객체는 boost::asio::io_service(또는 최신 버전에서는 boost::asio::io_context)와 밀접하게 연관되어 있으며, 이를 통해 I/O 작업을 실행하거나 대기한다.

소켓 예시

소켓 객체는 네트워크를 통해 데이터를 송수신하는 역할을 한다. 예를 들어 boost::asio::ip::tcp::socket은 TCP 연결을 다루는 I/O 객체이다. 소켓을 통해 비동기적으로 데이터를 송수신하려면, async_readasync_write와 같은 비동기 함수들을 사용하며, 이를 호출할 때 핸들러를 전달해야 한다.

핸들러는 비동기 작업이 완료될 때 호출되며, 결과를 처리하는 코드가 그 안에 포함된다. 예를 들어, 다음과 같은 흐름이 있다.

boost::asio::ip::tcp::socket socket(io_context);
socket.async_connect(endpoint, connect_handler);

이 코드에서 async_connect는 비동기 연결 작업을 시작하며, connect_handler는 연결 작업이 완료된 후 호출되는 콜백 함수이다.

핸들러의 정의와 역할

핸들러는 비동기 작업이 완료되었을 때 Boost.Asio가 호출하는 콜백 함수이다. 이 핸들러는 비동기 작업의 성공 여부와 결과를 처리하는 핵심 역할을 담당하며, 주로 다음과 같은 시나리오에서 사용된다.

  1. 비동기 작업의 성공 또는 실패 처리: 핸들러는 boost::system::error_code 인수를 통해 작업이 성공했는지 실패했는지 확인할 수 있다. 성공적이면, 작업 결과를 처리하고, 실패하면 오류 처리 절차를 실행한다.
  2. 연속적인 비동기 작업: 핸들러 안에서 또 다른 비동기 작업을 시작하여, 연속적인 비동기 작업을 체인으로 연결할 수 있다.

핸들러는 다음과 같은 형식으로 정의된다.

void handler(const boost::system::error_code& ec, std::size_t bytes_transferred);

이 핸들러 함수는 boost::system::error_code 객체를 통해 작업 상태를 확인하고, bytes_transferred와 같은 추가 정보를 받아들인다. 이는 주로 데이터 송수신 작업에서 몇 바이트가 처리되었는지를 나타내는 데 사용된다.

핸들러의 호출 흐름

비동기 작업을 시작하면, Boost.Asio는 해당 작업을 boost::asio::io_service에 등록하고, 작업이 완료되면 핸들러를 호출한다. 이때 핸들러는 작업 완료 시점의 상태 정보와 결과 데이터를 인수로 받는다.

graph LR A[비동기 작업 시작] --> B{I/O 작업 등록} B --> C[작업 대기] C --> D{작업 완료} D --> E[핸들러 호출]

위 다이어그램은 비동기 작업의 시작부터 핸들러 호출까지의 흐름을 나타낸다.

핸들러는 비동기 작업이 성공적으로 완료되었을 때 그 결과를 처리하거나, 오류가 발생했을 경우 적절한 예외 처리 또는 재시도를 할 수 있도록 설계된다.

핸들러의 특징

핸들러의 실행은 기본적으로 Boost.Asio의 io_context 객체에 의해 관리된다. io_context는 비동기 작업의 처리를 위해 이벤트 루프를 사용하며, 이 루프 안에서 핸들러를 실행한다. 이러한 구조 덕분에 비동기 작업을 쉽게 관리할 수 있다. 또한 핸들러의 다음과 같은 특징이 중요하다.

1. 비동기성 유지

핸들러는 작업이 완료되기 전까지 호출되지 않으며, 비동기 작업은 호출 즉시 완료되지 않는다. 이는 시스템의 자원을 효율적으로 사용하는 데 매우 유용하다. 예를 들어, 네트워크 I/O 작업은 상대적으로 느릴 수 있지만, 비동기적으로 처리되면 CPU는 다른 작업을 수행할 수 있다.

2. 핸들러의 경량성

Boost.Asio에서 핸들러는 매우 경량화되어 있으며, 이를 통해 여러 개의 비동기 작업을 동시에 처리할 수 있다. 핸들러는 콜백 함수이므로, 직접적인 스레드 관리를 하지 않아도 되는 이점이 있다. 즉, 별도의 스레드를 생성하지 않고도 비동기 작업을 처리할 수 있다.

3. 컨텍스트 정보 전달

핸들러에는 비동기 작업의 결과와 오류 정보를 담은 컨텍스트가 함께 전달된다. 이 정보를 통해 개발자는 비동기 작업의 성공 여부를 확인할 수 있으며, 필요에 따라 다음 작업을 결정할 수 있다. 예를 들어, boost::system::error_code를 사용해 오류가 발생했는지 확인하고, 성공적으로 완료된 경우에만 데이터를 처리할 수 있다.

핸들러와 io_context의 상호작용

핸들러는 boost::asio::io_context와 밀접한 관계를 가지고 있다. io_context는 비동기 작업을 관리하는 중심 객체로, 모든 비동기 작업은 io_context에 의해 처리된다. I/O 객체에서 비동기 작업을 시작하면, io_context가 이 작업을 큐에 등록하고, 해당 작업이 완료될 때 핸들러를 실행한다.

핸들러 실행 흐름

핸들러는 io_context의 이벤트 루프에서 실행되며, 이 루프는 작업이 완료되었을 때 큐에 있는 핸들러를 하나씩 처리한다. 이벤트 루프는 io_context::run() 함수로 구동되며, 비동기 작업이 발생할 때마다 이를 처리하고, 작업이 완료되면 대응되는 핸들러를 호출한다.

boost::asio::io_context io_context;

// I/O 작업 등록
boost::asio::ip::tcp::socket socket(io_context);
socket.async_connect(endpoint, connect_handler);

// io_context에서 작업 처리
io_context.run();

위 코드에서 io_context.run()은 비동기 작업을 처리하기 위한 이벤트 루프를 시작한다. 비동기 작업이 완료되면 connect_handler가 호출되고, 연결 성공 여부가 처리된다.

핸들러의 순차적 실행

여러 비동기 작업이 동일한 io_context에서 실행되는 경우, 핸들러는 순차적으로 실행된다. Boost.Asio는 핸들러 간의 경쟁 상태를 방지하기 위해 하나의 핸들러가 실행 중일 때 다른 핸들러가 실행되지 않도록 한다. 이러한 메커니즘은 비동기 작업의 안전한 처리를 보장한다.

graph LR A[비동기 작업 1 시작] --> B[핸들러 1 대기] C[비동기 작업 2 시작] --> D[핸들러 2 대기] B --> E[핸들러 1 실행] E --> F[작업 완료] F --> D[핸들러 2 실행]

위 다이어그램은 두 개의 비동기 작업이 순차적으로 실행되는 과정을 나타낸다. 핸들러는 작업이 완료될 때마다 실행되며, 동시에 실행되지 않는다.

핸들러와 오류 처리

핸들러에서 오류가 발생할 가능성을 고려해야 한다. Boost.Asio는 boost::system::error_code를 통해 비동기 작업 중 발생할 수 있는 오류를 관리하며, 핸들러의 첫 번째 인수로 이를 전달한다. 오류 처리는 비동기 작업의 안정성을 보장하는 중요한 단계로, 각 작업에 대해 올바른 오류 처리 방안을 마련해야 한다.

void connect_handler(const boost::system::error_code& ec) {
    if (!ec) {
        // 성공적으로 연결됨
    } else {
        // 오류 처리
        std::cerr << "Error: " << ec.message() << std::endl;
    }
}

위 코드에서 connect_handler는 비동기 연결 작업이 완료되면 호출되며, 오류가 발생하면 ec를 통해 처리된다. 오류 메시지는 ec.message()를 사용해 출력할 수 있다.

핸들러의 사용자 정의

Boost.Asio에서 핸들러는 자유롭게 정의할 수 있으며, 사용자 정의 핸들러를 통해 비동기 작업의 결과를 보다 구체적으로 처리할 수 있다. 이 핸들러는 단순한 함수 포인터일 수도 있고, 더 복잡한 클래스의 멤버 함수일 수도 있다. Boost.Asio는 다양한 형태의 핸들러를 지원하여 유연성을 제공한다.

함수 객체로 핸들러 정의

Boost.Asio의 핸들러는 함수 객체로 정의될 수 있다. 함수 객체는 함수 호출 연산자 operator()를 오버로드한 클래스를 의미하며, 이를 통해 핸들러에 상태 정보를 포함할 수 있다.

class MyHandler {
public:
    void operator()(const boost::system::error_code& ec, std::size_t bytes_transferred) {
        if (!ec) {
            std::cout << "Successfully transferred " << bytes_transferred << " bytes.\n";
        } else {
            std::cerr << "Error: " << ec.message() << std::endl;
        }
    }
};

이 예시에서 MyHandler 클래스는 operator()를 오버로드하여 핸들러 역할을 한다. 비동기 작업이 완료되면 이 객체가 호출되어 결과를 처리한다. 함수 객체를 사용하면 상태를 클래스 내부에 유지할 수 있으며, 핸들러가 실행될 때 해당 상태에 접근할 수 있는 장점이 있다.

멤버 함수로 핸들러 정의

핸들러는 클래스의 멤버 함수로도 정의될 수 있다. Boost.Asio는 클래스 멤버 함수도 핸들러로 사용할 수 있도록 지원한다. 다만, 멤버 함수는 객체 인스턴스와 함께 사용되어야 하므로, std::bind 또는 boost::bind를 이용하여 객체와 함수 포인터를 결합해야 한다.

class MyClass {
public:
    void handle_read(const boost::system::error_code& ec, std::size_t bytes_transferred) {
        if (!ec) {
            std::cout << "Read " << bytes_transferred << " bytes.\n";
        } else {
            std::cerr << "Error: " << ec.message() << std::endl;
        }
    }
};

// 사용 예시
boost::asio::ip::tcp::socket socket(io_context);
MyClass my_object;
socket.async_read_some(boost::asio::buffer(data), std::bind(&MyClass::handle_read, &my_object, std::placeholders::_1, std::placeholders::_2));

위 예제에서 MyClasshandle_read 멤버 함수가 핸들러로 사용되며, std::bind를 통해 객체 my_object와 결합된다. 이 방법을 사용하면 클래스 내부에서 상태 정보를 쉽게 관리하고, 핸들러가 실행될 때 해당 상태를 활용할 수 있다.

핸들러의 지속성 관리

Boost.Asio에서 핸들러는 비동기 작업이 완료될 때까지 반드시 유효한 상태여야 한다. 비동기 작업은 즉시 완료되지 않기 때문에, 핸들러가 파괴되면 작업이 완료되었을 때 이를 처리할 수 없게 된다. 따라서 핸들러의 지속성, 즉 작업이 끝날 때까지 핸들러가 유효하게 남아 있어야 하는 문제가 발생한다.

이를 해결하는 대표적인 방법은 std::shared_ptr을 사용하는 것이다. std::shared_ptr은 참조 횟수를 관리하여 객체가 더 이상 사용되지 않을 때 자동으로 삭제되는 메커니즘을 제공한다. 핸들러가 비동기 작업과 함께 사용될 때, 핸들러가 파괴되지 않도록 std::shared_ptr을 사용하여 참조를 유지할 수 있다.

std::shared_ptr을 이용한 핸들러 지속성

class MyHandler : public std::enable_shared_from_this<MyHandler> {
public:
    void start() {
        auto self(shared_from_this());
        socket.async_read_some(boost::asio::buffer(data), 
            [self](const boost::system::error_code& ec, std::size_t bytes_transferred) {
                self->handle_read(ec, bytes_transferred);
            });
    }

    void handle_read(const boost::system::error_code& ec, std::size_t bytes_transferred) {
        if (!ec) {
            std::cout << "Read " << bytes_transferred << " bytes.\n";
        } else {
            std::cerr << "Error: " << ec.message() << std::endl;
        }
    }

private:
    boost::asio::ip::tcp::socket socket;
    std::array<char, 128> data;
};

이 예제에서 MyHandler 클래스는 std::enable_shared_from_this를 상속받아, 자신을 가리키는 shared_ptr을 얻을 수 있다. 이를 통해 핸들러의 유효성을 보장하고, 비동기 작업이 완료될 때까지 핸들러 객체가 유지된다. 핸들러는 비동기 작업이 완료될 때까지 파괴되지 않으며, 작업이 끝나면 자동으로 참조 횟수가 감소하여 메모리에서 해제된다.

핸들러 체이닝

비동기 작업에서 핸들러 체이닝은 매우 중요한 개념이다. 핸들러 체이닝이란, 하나의 비동기 작업이 완료된 후, 다른 비동기 작업을 시작하는 방식으로 핸들러를 연결하는 것이다. 이를 통해 비동기 작업의 순차적 실행을 제어할 수 있으며, 복잡한 비동기 흐름을 구성할 수 있다.

void first_handler(const boost::system::error_code& ec) {
    if (!ec) {
        socket.async_read_some(boost::asio::buffer(data), second_handler);
    }
}

void second_handler(const boost::system::error_code& ec, std::size_t bytes_transferred) {
    if (!ec) {
        std::cout << "Successfully read " << bytes_transferred << " bytes.\n";
    }
}

위 예제에서 첫 번째 핸들러 first_handler가 실행된 후, socket.async_read_some을 호출하여 두 번째 핸들러 second_handler를 연결한다. 이러한 방식으로 비동기 작업을 체인처럼 연결하여 순차적으로 실행할 수 있다.

핸들러 체이닝과 제어 흐름

핸들러 체이닝은 비동기 작업의 순차적 제어를 구현하는데 매우 유용하지만, 제어 흐름을 복잡하게 만들 수 있다. 특히 여러 개의 비동기 작업이 서로 의존적인 경우, 핸들러 체이닝은 적절한 설계가 필요하다.

상태 기반 핸들러 체이닝

비동기 작업의 제어 흐름을 보다 명확하게 관리하기 위해, 상태 기반 접근 방식을 사용할 수 있다. 이를 통해 작업 간의 상태를 명확하게 구분하고, 각 상태에서 다음 비동기 작업을 어떻게 처리할지를 정의할 수 있다.

enum class State {
    INIT,
    CONNECTED,
    DATA_RECEIVED,
    DONE
};

class Connection {
public:
    Connection(boost::asio::io_context& io_context) 
        : socket(io_context), current_state(State::INIT) {}

    void start() {
        socket.async_connect(endpoint, std::bind(&Connection::handle_connect, this, std::placeholders::_1));
    }

private:
    void handle_connect(const boost::system::error_code& ec) {
        if (!ec) {
            current_state = State::CONNECTED;
            socket.async_read_some(boost::asio::buffer(data), 
                std::bind(&Connection::handle_read, this, std::placeholders::_1, std::placeholders::_2));
        } else {
            std::cerr << "Connection error: " << ec.message() << std::endl;
        }
    }

    void handle_read(const boost::system::error_code& ec, std::size_t bytes_transferred) {
        if (!ec) {
            current_state = State::DATA_RECEIVED;
            std::cout << "Read " << bytes_transferred << " bytes.\n";
            current_state = State::DONE;
        } else {
            std::cerr << "Read error: " << ec.message() << std::endl;
        }
    }

    boost::asio::ip::tcp::socket socket;
    std::array<char, 128> data;
    State current_state;
};

위 코드에서는 Connection 클래스가 비동기 작업을 상태 기반으로 관리한다. current_state 변수는 각 작업의 진행 상태를 나타내며, 상태에 따라 적절한 작업을 수행한다. 이러한 상태 기반 핸들러 체이닝은 복잡한 비동기 작업 흐름을 보다 체계적으로 관리할 수 있도록 돕는다.

비동기 작업의 중첩 처리

비동기 작업을 중첩 처리하는 경우도 자주 발생한다. 예를 들어, 데이터 송수신 작업에서, 데이터를 보내는 작업과 받는 작업이 동시에 진행될 수 있다. 이때 두 작업을 독립적으로 관리하면서도 서로 중첩되지 않도록 처리해야 한다.

void handle_write(const boost::system::error_code& ec, std::size_t bytes_transferred) {
    if (!ec) {
        std::cout << "Sent " << bytes_transferred << " bytes.\n";
        socket.async_read_some(boost::asio::buffer(data), handle_read);
    } else {
        std::cerr << "Write error: " << ec.message() << std::endl;
    }
}

void handle_read(const boost::system::error_code& ec, std::size_t bytes_transferred) {
    if (!ec) {
        std::cout << "Received " << bytes_transferred << " bytes.\n";
        socket.async_write_some(boost::asio::buffer(data), handle_write);
    } else {
        std::cerr << "Read error: " << ec.message() << std::endl;
    }
}

이 예제에서는 데이터를 송신한 후 바로 수신 작업을 시작하고, 데이터를 수신한 후 다시 송신 작업을 시작하는 방식으로 중첩 작업이 이루어진다. 이러한 형태의 비동기 작업 중첩 처리는 일반적인 네트워크 통신에서 자주 사용된다.

핸들러에서 오류 전파

비동기 작업 중 오류가 발생했을 때, 이를 적절히 처리하고 이후 작업에 반영해야 한다. Boost.Asio에서는 오류가 발생하면 boost::system::error_code가 핸들러에 전달되며, 이를 통해 오류 처리를 수행할 수 있다. 하지만 경우에 따라 오류를 다른 핸들러나 작업으로 전파해야 할 수도 있다.

오류 전파를 위한 핸들러 설계

비동기 작업에서 오류가 발생하면, 이를 처리하고 이후 작업에 영향을 주지 않도록 해야 한다. 일반적으로는 오류가 발생한 경우, 더 이상의 작업을 중단하거나, 적절한 로그 메시지를 남기고 종료한다. 하지만 경우에 따라 오류를 무시하고 작업을 계속할 수도 있다.

void handle_read(const boost::system::error_code& ec, std::size_t bytes_transferred) {
    if (!ec) {
        std::cout << "Received " << bytes_transferred << " bytes.\n";
        // 다음 작업 계속
    } else if (ec == boost::asio::error::operation_aborted) {
        std::cerr << "Operation aborted: " << ec.message() << std::endl;
    } else {
        std::cerr << "Read error: " << ec.message() << std::endl;
    }
}

위 코드에서 오류가 발생했을 때, 특정 오류 코드(boost::asio::error::operation_aborted)를 확인하고 그에 맞게 처리할 수 있다. 이처럼 오류 처리를 세분화하여 오류의 종류에 따라 다른 방식으로 핸들링할 수 있다.

비동기 작업 재시도

비동기 작업 중 오류가 발생했을 때, 단순히 작업을 중단하는 대신 재시도하는 경우도 있다. 예를 들어, 네트워크 연결 오류가 일시적일 수 있으므로, 재시도를 통해 문제가 해결될 수 있다.

void handle_connect(const boost::system::error_code& ec) {
    if (!ec) {
        std::cout << "Connected.\n";
    } else {
        std::cerr << "Connection failed, retrying...\n";
        socket.async_connect(endpoint, handle_connect);  // 재시도
    }
}

이 코드에서 연결 작업이 실패했을 경우, 다시 async_connect를 호출하여 연결을 재시도한다. 재시도 전략은 비동기 작업의 안정성을 높이는 방법 중 하나이다.

핸들러에서의 자원 관리

비동기 작업이 여러 개 동시에 실행되거나, 각 작업에서 큰 데이터를 다루는 경우, 자원 관리가 중요한 이슈가 될 수 있다. 핸들러가 실행되는 동안 관리되는 메모리나 시스템 자원을 효율적으로 관리해야만, 불필요한 자원 낭비나 메모리 누수를 방지할 수 있다.

스마트 포인터를 통한 메모리 관리

앞서 언급한 std::shared_ptr과 같은 스마트 포인터는 핸들러가 비동기 작업 중에도 유효하게 유지되도록 돕는다. 이를 통해 자원이 적절히 해제될 수 있게 관리할 수 있다. 이 방법은 특히 비동기 작업이 완료될 때까지 객체의 생명 주기를 연장해야 할 때 유용하다.

예를 들어, 아래와 같이 스마트 포인터를 사용하면 핸들러가 비동기 작업 중에도 안전하게 객체에 접근할 수 있다.

class Session : public std::enable_shared_from_this<Session> {
public:
    Session(boost::asio::ip::tcp::socket socket)
        : socket_(std::move(socket)) {}

    void start() {
        auto self(shared_from_this());
        socket_.async_read_some(boost::asio::buffer(data_), 
            [self](const boost::system::error_code& ec, std::size_t length) {
                self->handle_read(ec, length);
            });
    }

private:
    void handle_read(const boost::system::error_code& ec, std::size_t length) {
        if (!ec) {
            std::cout << "Read " << length << " bytes\n";
        } else {
            std::cerr << "Read error: " << ec.message() << std::endl;
        }
    }

    boost::asio::ip::tcp::socket socket_;
    std::array<char, 128> data_;
};

위 코드에서 shared_from_this()를 사용하여 Session 객체의 수명을 관리하고, 핸들러 내에서도 안전하게 객체에 접근할 수 있다. 이 방식은 특히 여러 개의 비동기 작업이 동일한 객체를 참조하는 경우에 효과적이다.

핸들러에서의 메모리 할당 최소화

비동기 작업을 자주 실행하는 애플리케이션에서는 핸들러에서의 메모리 할당이 성능 병목을 초래할 수 있다. 이를 해결하기 위해 메모리 풀을 활용하거나 메모리 재사용 전략을 사용할 수 있다. Boost.Asio는 boost::asio::handler_alloc_hook를 통해 커스텀 메모리 할당자를 제공하는 기능을 갖추고 있다.

메모리 할당 후 매번 새로운 메모리 블록을 할당하는 대신, 이전에 사용된 메모리를 재사용할 수 있는 전략을 도입하는 방식이다.

template <typename Handler>
class CustomAllocator {
public:
    CustomAllocator() : storage_(nullptr) {}

    void* allocate(std::size_t size) {
        if (!storage_ && size <= sizeof(storage_)) {
            return &storage_;
        }
        return ::operator new(size);
    }

    void deallocate(void* pointer, std::size_t size) {
        if (pointer != &storage_) {
            ::operator delete(pointer);
        }
    }

private:
    typename std::aligned_storage<64>::type storage_;
};

위 예제는 간단한 메모리 할당자이며, 메모리 풀이 없을 때 새로운 메모리 블록을 할당하지만, 필요한 경우 동일한 메모리 블록을 재사용할 수 있다. 이 방식을 핸들러에 적용하면, 비동기 작업에서의 메모리 할당 비용을 줄일 수 있다.

핸들러와 비동기 스트림 처리

핸들러는 단순히 단일 비동기 작업에만 사용되는 것이 아니라, 비동기 스트림 처리에도 중요한 역할을 한다. 예를 들어, 네트워크 소켓을 통해 데이터를 연속적으로 주고받거나 파일에서 연속적으로 데이터를 읽어 들이는 경우, 핸들러는 이러한 스트림 작업을 관리하는 데 매우 유용하다.

비동기 스트림 읽기

비동기 스트림 처리의 대표적인 예는 파일에서 데이터를 읽어 들이거나 네트워크 소켓을 통해 데이터를 수신하는 것이다. Boost.Asio에서 제공하는 비동기 읽기 함수 async_read는 핸들러와 함께 사용할 수 있으며, 데이터를 스트림 단위로 읽어 들인다.

void start_reading() {
    boost::asio::async_read(socket_, boost::asio::buffer(data_), 
        [this](const boost::system::error_code& ec, std::size_t bytes_transferred) {
            handle_read(ec, bytes_transferred);
        });
}

void handle_read(const boost::system::error_code& ec, std::size_t bytes_transferred) {
    if (!ec) {
        std::cout << "Read " << bytes_transferred << " bytes\n";
        // 더 많은 데이터를 읽기 위해 다시 호출
        start_reading();
    } else {
        std::cerr << "Error on read: " << ec.message() << std::endl;
    }
}

이 코드에서는 데이터를 비동기적으로 읽어 들이고, 작업이 완료된 후 다시 start_reading을 호출하여 다음 스트림 데이터를 처리한다. 이 방식으로 연속적인 스트림 작업을 처리할 수 있다.

비동기 스트림 쓰기

비동기 스트림 작업은 읽기뿐만 아니라 쓰기 작업에도 적용된다. Boost.Asio의 async_write 함수는 핸들러를 사용하여 데이터를 스트림 단위로 쓰는 작업을 처리할 수 있다.

void start_writing() {
    boost::asio::async_write(socket_, boost::asio::buffer(data_to_send), 
        [this](const boost::system::error_code& ec, std::size_t bytes_transferred) {
            handle_write(ec, bytes_transferred);
        });
}

void handle_write(const boost::system::error_code& ec, std::size_t bytes_transferred) {
    if (!ec) {
        std::cout << "Sent " << bytes_transferred << " bytes\n";
        // 더 많은 데이터를 전송하기 위해 다시 호출 가능
    } else {
        std::cerr << "Error on write: " << ec.message() << std::endl;
    }
}

이 코드는 비동기적으로 데이터를 송신하고, 송신 작업이 완료되면 핸들러를 통해 송신된 바이트 수를 확인하는 구조를 가지고 있다. 핸들러를 활용하여 스트림 작업을 지속적으로 이어 나갈 수 있다.

핸들러의 동시성 문제

Boost.Asio는 단일 스레드에서 작동하는 구조를 기본으로 하므로, 핸들러의 동시성 문제는 상대적으로 적다. 그러나 다중 스레드 환경에서 여러 스레드가 동일한 io_context를 공유하는 경우, 동시성 문제가 발생할 수 있다. 이때는 핸들러 실행 중 자원을 보호하기 위해 동기화 메커니즘을 적용해야 한다.

strand를 통한 동시성 제어

Boost.Asio는 boost::asio::strand라는 개념을 도입하여 동시성 문제를 해결할 수 있다. strand는 특정 작업을 순차적으로 실행하게 만들어, 여러 스레드가 같은 자원을 공유할 때 동시성 문제를 방지한다.

boost::asio::strand<boost::asio::io_context::executor_type> strand(io_context.get_executor());

socket_.async_read_some(boost::asio::buffer(data_), 
    boost::asio::bind_executor(strand, 
        [this](const boost::system::error_code& ec, std::size_t bytes_transferred) {
            handle_read(ec, bytes_transferred);
        }));

위 코드에서는 strand를 사용하여 async_read_some 작업이 순차적으로 실행되도록 보장한다. strand는 동일한 io_context 내에서 여러 스레드가 동시에 접근하더라도, 작업이 중첩되지 않고 순차적으로 처리되도록 보장하는 역할을 한다.