Timer.5 - 멀티스레드 프로그램에서 핸들러 동기화 (Synchronising handlers in multithreaded programs)
이 튜토리얼은 멀티스레드 프로그램에서 콜백 핸들러를 동기화하기 위해 스트랜드(strand) 클래스 템플릿을 사용하는 방법을 보여준다.
이전 네 개의 튜토리얼은 하나의 스레드에서만 io_context::run() 함수를 호출하여 핸들러 동기화 문제를 회피했다. 이미 알고 있듯이, asio 라이브러리는 콜백 핸들러가 현재 io_context::run()을 호출하는 스레드에서만 호출되도록 보장한다. 따라서 하나의 스레드에서만 io_context.run()을 호출하면 콜백 핸들러가 동시에 실행될 수 없다.
단일 스레드 접근 방식은 asio를 사용하여 응용프로그램을 개발할 때 일반적으로 시작하기에 좋은 방법이다. 단점은 프로그램, 특히 서버에 미치는 다음과 같은 제한 사항이다:
- 핸들러를 완료하는데 오랜 시간이 걸릴 수 있는 경우 응답성이 떨어진다.
- 멀티 프로세서 시스템에서 확장할 수 없다.
이러한 제한 사항에 직면한 경우, 다른 방법은 io_context::run()을 호출하는 스레드 풀을 갖는 것이다. 그러나 이렇게 하면 핸들러가 동시에 실행될 수 있으므로 핸들러가 공유된 리소스 또는 스레드에 안전하지 않은 리소스에 접근할 수 있을 때 동기화 방법이 필요하다.
#include <iostream>
#include <boost/asio.hpp>
#include <boost/thread/thread.hpp>
#include <boost/bind/bind.hpp>
이전 튜토리얼의 클래스와 유사한 printer라는 클래스를 정의하여 시작한다. 이 클래스는 두 개의 타이머를 병렬로 실행하여 이전 튜토리얼을 확장할 수 있다.
class printer
{
public:
한 쌍의 boost::asio::steady_timer 멤버를 초기화하는 것 외에도, 생성자는 boost::asio::strand<boost::asio::io_context::executor_type> 유형의 개체인 strand_ 멤버를 초기화한다.
스트랜드(strand) 클래스 템플릿은, 이 템플릿을 통해 전달된(dispatched) 실행 중인 핸들러가 다음 핸들러가 시작되기 전에 완료될 수 있도록 보장하는 실행기(executor) 어댑터(adapter)이다. 이것은 io_context::run()을 호출하는 스레드의 수에 관계없이 보장된다. 물론, 핸들러는 스트랜드(strand)를 통해 전달되지(dispatched) 않거나 다른 스트랜드(strand) 개체를 통해 전달된(dispatched) 다른 핸들러와 여전히 동시에 실행할 수 있다.
printer(boost::asio::io_context& io)
: strand_(boost::asio::make_strand(io)),
timer1_(io, boost::asio::chrono::seconds(1)),
timer2_(io, boost::asio::chrono::seconds(1)),
count_(0)
{
비동기 작업을 시작할 때, 각 콜백 핸들러는 boost::asio::strand<boost::asio::io_context::executor_type> 개체에 "bound(바인드)" 된다. boost::asio::bind_executor() 함수는, 함수에 포함된 핸들러를 스트랜드(strand) 개체를 통해 자동으로 전달하는(dispatches) 새로운 핸들러를 반환한다. 핸들러를 같은 스트랜드(strand)에 바인딩하여, 동시에 실행할 수 없도록 한다.
timer1_.async_wait(boost::asio::bind_executor(strand_,
boost::bind(&printer::print1, this)));
timer2_.async_wait(boost::asio::bind_executor(strand_,
boost::bind(&printer::print2, this)));
}
~printer()
{
std::cout << "Final count is " << count_ << std::endl;
}
멀티스레드 프로그램에서, 비동기 작업에 대한 핸들러는 공유된 리소스에 접근하는 경우 동기화되어야 한다. 이 튜토리얼에서, 핸들러(print1과 print2)가 사용하는 공유된 리소스는 std::cout 및 count_ 데이터 멤버이다.
void print1()
{
if (count_ < 10)
{
std::cout << "Timer 1: " << count_ << std::endl;
++count_;
timer1_.expires_at(timer1_.expiry() + boost::asio::chrono::seconds(1));
timer1_.async_wait(boost::asio::bind_executor(strand_,
boost::bind(&printer::print1, this)));
}
}
void print2()
{
if (count_ < 10)
{
std::cout << "Timer 2: " << count_ << std::endl;
++count_;
timer2_.expires_at(timer2_.expiry() + boost::asio::chrono::seconds(1));
timer2_.async_wait(boost::asio::bind_executor(strand_,
boost::bind(&printer::print2, this)));
}
}
private:
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
boost::asio::steady_timer timer1_;
boost::asio::steady_timer timer2_;
int count_;
};
main 함수는 이제 io_context::run()이 두 개의 스레드에서(메인 스레드와 추가 스레드) 호출되도록 한다. 이는 boost::thread 객체를 사용하여 수행한다.
단일 스레드에서 호출할 때와 마찬가지로, "work(작업)"이 남아있는 동안 io_context::run()에 대한 동시 호출이 계속 실행된다. 모든 비동기 작업이 완료될 때까지 백그라운드 스레드는 종료되지 않는다.
int main()
{
boost::asio::io_context io;
printer p(io);
boost::thread t(boost::bind(&boost::asio::io_context::run, &io));
io.run();
t.join();
return 0;
}
전체 소스 보기
돌아가기: 튜토리얼 페이지
이전: Timer.4 - 핸들러로 멤버 함수를 사용
원본 링크
'Boost C++ Libraries > Boost.Asio' 카테고리의 다른 글
Boost.Asio 튜토리얼 - Daytime.1 - 동기식 TCP daytime 클라이언트 (0) | 2020.12.16 |
---|---|
Boost.Asio 튜토리얼 - Timer.5 소스 (0) | 2020.12.16 |
Boost.Asio 튜토리얼 - Timer.4 소스 (0) | 2020.12.15 |
Boost.Asio 튜토리얼 - Timer.4 - 핸들러로 멤버 함수를 사용 (0) | 2020.12.15 |
Boost.Asio 튜토리얼 - Timer.3 소스 (0) | 2020.12.15 |