본문 바로가기

C++/Boost

boost 비동기 이론

[1. 개요]

boost 에서 비동기 프로그램 작성에 대한 배경 지식을 정리하도록 한다.

  • io_context
  • strand
  • std::enable_shared_from_this

논블로킹 IO 와 달리 비동기 IO 는 별도의 스레드로 동작하는 이벤트 루프가 필요하다.

  • 논블로킹은 일종의 폴링 과 같은 방식이기 때문이다.

[2. io_context]

boost 에서 io_context 는 내부적으로 큐를 갖고 있고, 이 큐에 비동기 작업이 등록되며

io_context.run() 을 호출하면 큐에 쌓인 작업을 하나씩 꺼내서 실행하게 된다.

std::vector<std::shared_ptr<std::thread>> threads;
for (unsigned i = 0; i < thread_pool_size; ++i)
{
    std::shared_ptr<std::thread> thread = std::make_shared<std::thread>(
        // 멀티 스레드를 통해
        // io_context 의 run() 메소드 호출
        boost::bind(&boost::asio::io_context::run, &io_context));
    threads.push_back(thread);
}
for (const auto &thread : threads)
{
    thread->join();
}

 

위와 같이 멀티 스레드를 이용하면 병렬 처리도 가능하다.

 

그러나 한가지 문제가 있는데,

여러 스레드가 동시에 콜백 핸들러를 실행하므로 공유 자원 접근 시 동기화 문제가 필요하게 된다.

  • 예를들어, 어떤 객체가 비동기 작업 후 실행 할 콜백을 설정하는데,
  • 이 콜백은 객체의 멤버 함수이고, 이 멤버 함수에서 객체의 멤버 변수 접근이 발생
  • 이러한 콜백이 여러 개 라면...

[3. strand]

strand 는 이러한 동일 리소스를 여러 핸들러가 동식에 접근하지 않도록 직렬 실행을 보장 해준다.

동일 strand 를 통해 등록된 핸들러(콜백) 들은 서로 겹치지 않고 순차 실행 하는 것을 보장 한다.

# 내부적으로 Lock 및 atomic 연산을 사용한다고 함.

# 등록 시 Lock 을 잠깐 사용하고, 
# 콜백 실행 시에는 락이 없어 성능 저하가 크지 않다고 함.

 

과거에는 boost::asio::io_context::strand 를 이용했지만,

현재는 boost::asio::strand 를 사용한다.

  • boost::asio::io_context io;
  • boost::asio::strand<boost::asio::io_context::executor_type> strand(io.get_executor());
  • 혹은
  • boost::asio::make_strand(io);
  • 그래서 주로 현재 비동기 객체에서,
  • async_read 와 async_write 같은 비동기 호출이 동시에 발생 할 수 있는 경우 
  • strand.wrap 을 이용해서 콜백을 등록하는 것이 안전하다.

read 후 write 하는 순서가 명확하다면, 굳이 wrap 을 이용해서 콜백을 등록하지 않아도 된다.

TCP_socket.async_read_some(boost::asio::buffer(incoming_data_buffer),
                           boost::bind(&Connection::handle_read,
                                       this->shared_from_this(),
                                       boost::asio::placeholders::error,
                                       boost::asio::placeholders::bytes_transferred));

[4. std::enable_shared_from_this]

비동기 프로그램 작성 시 하나의 원칙이 있다면, 

비동기 함수 호출에 사용 된 모든 인자들은 콜백 실행 시점에서도 그 수명이 있음을 보장해야 한다는 것이다. 

 

위 예제에서 incoming_data_buffer 라는 버퍼가

Connection 객체의  handle_read() 함수 호출 시점에 존재하지 않는다면 문제가 된다.

 

따라서, 비동기 호출에 사용 된 모든 값/객체 들에 대해서 생명 주기를 보장해야 한다.

이러한 점에서 smart pointer (shared_ptr) 가 사용되는 것이 매우 적합하다.

 

특히, shared_ptr 은 더 이상 참조되지 않는 순간 실제 객체가 소멸되기에 메모리 관리 측면에서도 효율적이다.

그러나, shared_ptr 이 가리키는 객체 안에서 이 객체의 shared_ptr 을 가져오는 것은 불가능한데

이 때 사용하는 것이 바로 std::enable_shared_from_this 이다.

 

이 템플릿 클래스를 상속하는 클래스는 내부 멤버 함수에서 shared_from_this() 를 호출하여

자기 자신의 shared_ptr 을 가져와 ref count 를 증가 시켜 콜백 호출 시점에서도 객체가 소멸되지 않음을 보장 할 수 있다.


[5. 정리]

 

'C++ > Boost' 카테고리의 다른 글

Boost 라이브러리 설치  (0) 2022.07.16