Fundacja
Zacznijmy od uproszczonego przykładu i przeanalizujmy odpowiednie elementy Boost.Asio:
void handle_async_receive(...) { ... }
void print() { ... }
...
boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);
...
io_service.post(&print);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
Co to jest Handler ?
Program obsługi to nic innego jak wywołanie zwrotne. W przykładowym kodzie są 3 programy obsługi:
- Przewodnik
print(1).
- Przewodnik
handle_async_receive(3).
- Przewodnik
print(4).
Mimo że ta sama print()funkcja jest używana dwukrotnie, każde użycie jest uważane za utworzenie własnej, unikatowej procedury obsługi. Programy obsługi mogą mieć wiele kształtów i rozmiarów, od podstawowych funkcji, takich jak te powyżej, do bardziej złożonych konstrukcji, takich jak funktory generowane z boost::bind()i lambdy. Niezależnie od złożoności, program obsługi nadal pozostaje niczym więcej niż wywołaniem zwrotnym.
Co to jest praca ?
Praca to przetwarzanie, o wykonanie którego Boost.Asio został poproszony w imieniu kodu aplikacji. Czasami Boost.Asio może rozpocząć część pracy, gdy tylko zostanie o tym poinformowana, a innym razem może zaczekać na wykonanie pracy w późniejszym czasie. Po zakończeniu pracy Boost.Asio poinformuje aplikację, wywołując dostarczoną procedurę obsługi .
Boost.Asio gwarantuje, że koparki będzie działać tylko w wątku, który jest obecnie wywołującego run(), run_one(), poll(), lub poll_one(). To są wątki, które będą działać i programy obsługi połączeń . Dlatego w powyższym przykładzie print()nie jest wywoływana, gdy jest wysyłana do io_service(1). Zamiast tego jest dodawany do io_servicei zostanie wywołany w późniejszym czasie. W tym przypadku w ciągu io_service.run()(5).
Co to są operacje asynchroniczne?
Operacja asynchroniczna tworzy pracę, a Boost.Asio wywoła procedurę obsługi, aby poinformować aplikację o zakończeniu pracy. Operacje asynchroniczne są tworzone przez wywołanie funkcji, która ma nazwę z prefiksem async_. Funkcje te są również znane jako funkcje inicjujące .
Operacje asynchroniczne można rozłożyć na trzy unikalne kroki:
- Zainicjowanie lub poinformowanie powiązanego,
io_serviceże działa, musi zostać wykonane. async_receiveOperacja (3) informuje io_service, że będzie trzeba asynchronicznie odczytu danych z gniazda, a następnie async_receivezwraca natychmiast.
- Wykonuję właściwą pracę. W takim przypadku po
socketotrzymaniu danych bajty zostaną odczytane i skopiowane do buffer. Rzeczywista praca zostanie wykonana w:
- Funkcja inicjująca (3), jeśli Boost.Asio może określić, że nie będzie blokować.
- Gdy aplikacja jawnie uruchamia
io_service(5).
- Wywołanie
handle_async_receive ReadHandler . Po raz kolejny programy obsługi są wywoływane tylko w wątkach z uruchomionym io_service. Zatem niezależnie od tego, kiedy praca zostanie wykonana (3 lub 5), gwarantuje się, że handle_async_receive()zostanie ona wywołana tylko w ciągu io_service.run()(5).
Oddzielenie w czasie i przestrzeni między tymi trzema etapami jest znane jako inwersja przepływu sterowania. Jest to jedna ze złożoności, która utrudnia programowanie asynchroniczne. Istnieją jednak techniki, które mogą pomóc w złagodzeniu tego problemu , na przykład za pomocą programów .
Co robi io_service.run()?
Gdy wywoła wątek io_service.run(), praca i programy obsługi będą wywoływane z poziomu tego wątku. W powyższym przykładzie io_service.run()(5) będzie blokować do:
- Wywołał i zwrócił z obu
printprogramów obsługi, operacja odbierania zakończyła się sukcesem lub niepowodzeniem, a jej handle_async_receiveprogram obsługi został wywołany i zwrócony.
io_serviceWyraźnie zatrzymała io_service::stop().
- Wyjątek jest zgłaszany z wnętrza programu obsługi.
Jeden potencjalny pseudo-przepływ można opisać następująco:
utwórz io_service
utwórz gniazdo
dodaj moduł obsługi drukowania do io_service (1)
poczekaj, aż gniazdo się połączy (2)
dodaj asynchroniczne żądanie odczytu pracy do io_service (3)
dodaj obsługę drukowania do io_service (4)
uruchom io_service (5)
czy jest tam praca lub opiekunowie?
tak, jest 1 praca i 2 opiekunów
czy gniazdo ma dane? nie, nic nie rób
uruchom program obsługi drukowania (1)
czy jest tam praca lub opiekunowie?
tak, jest 1 praca i 1 przewodnik
czy gniazdo ma dane? nie, nic nie rób
uruchom program obsługi drukowania (4)
czy jest tam praca lub opiekunowie?
tak, jest 1 praca
czy gniazdo ma dane? nie, czekaj dalej
- gniazdo odbiera dane -
gniazdo zawiera dane, wczytaj je do bufora
dodaj program obsługi handle_async_receive do io_service
czy jest tam praca lub opiekunowie?
tak, jest 1 przewodnik
uruchom handle_async_receive handler (3)
czy jest tam praca lub opiekunowie?
nie, ustaw io_service jako zatrzymane i wróć
Zwróć uwagę, jak po zakończeniu odczytu dodano kolejną procedurę obsługi do io_service. Ten subtelny szczegół jest ważną cechą programowania asynchronicznego. Pozwala on na koparki być przykuty razem. Na przykład, jeśli handle_async_receivenie otrzyma wszystkich oczekiwanych danych, jego implementacja może wysłać kolejną asynchroniczną operację odczytu, co spowoduje io_servicewięcej pracy, a tym samym nie powróci z io_service.run().
Zwróć uwagę, że gdy io_serviceskończy się praca, aplikacja musi reset()ją io_serviceprzed ponownym uruchomieniem.
Przykładowe pytanie i przykładowy kod 3a
Teraz przyjrzyjmy się dwóm fragmentom kodu, do których odwołuje się pytanie.
Kod pytania
socket->async_receivedodaje pracę do io_service. W ten sposób io_service->run()będzie blokować, dopóki operacja odczytu nie zakończy się sukcesem lub błędem i ClientReceiveEventalbo zakończy działanie, albo zgłosi wyjątek.
W nadziei na ułatwienie zrozumienia, oto mniejszy przykład 3a z adnotacjami:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
Na wysokim poziomie program utworzy 2 wątki, które będą przetwarzać io_servicepętlę zdarzeń (2). Daje to prostą pulę wątków, która obliczy liczby Fibonacciego (3).
Jedną główną różnicą między kodem pytania a tym kodem jest to, że ten kod wywołuje io_service::run()(2) przed dodaniem rzeczywistej pracy i programów obsługi do io_service(3). Aby zapobiec io_service::run()natychmiastowemu powracaniu, io_service::worktworzony jest obiekt (1). Ten obiekt zapobiega io_servicebrakowi pracy; dlatego io_service::run()nie powróci w wyniku braku pracy.
Ogólny przepływ jest następujący:
- Utwórz i dodaj
io_service::workobiekt dodany do io_service.
- Utworzono pulę wątków, która wywołuje
io_service::run(). Te wątki robocze nie powrócą z io_servicepowodu io_service::workobiektu.
- Dodaj 3 procedury obsługi, które obliczają liczby Fibonacciego
io_service, i natychmiast wróć. Wątki robocze, a nie wątek główny, mogą natychmiast rozpocząć uruchamianie tych programów obsługi.
- Usuń
io_service::workobiekt.
- Poczekaj, aż wątki robocze zostaną zakończone. Nastąpi to dopiero, gdy wszystkie 3 procedury obsługi zakończą wykonywanie, ponieważ
io_serviceżaden z nich nie ma obsługi ani nie działa.
Kod można napisać inaczej, w taki sam sposób jak kod oryginalny, w którym procedury obsługi są dodawane do elementu io_service, a następnie io_serviceprzetwarzana jest pętla zdarzeń. Eliminuje to potrzebę używania io_service::worki skutkuje następującym kodem:
int main()
{
boost::asio::io_service io_service;
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
Synchroniczne a asynchroniczne
Chociaż kod w pytaniu używa operacji asynchronicznej, skutecznie działa synchronicznie, ponieważ czeka na zakończenie operacji asynchronicznej:
socket.async_receive(buffer, handler)
io_service.run();
jest równa:
boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);
Zgodnie z ogólną zasadą należy unikać mieszania operacji synchronicznych i asynchronicznych. Często może zmienić złożony system w skomplikowany system. Ta odpowiedź podkreśla zalety programowania asynchronicznego, z których niektóre są również omówione w dokumentacji Boost.Asio .