2016-07-29 13 views
13

Im folgenden Code erwarte ich, dass die Ausgabe immer 1 ist, weil ich erwarte, dass nur ein Handler ausgeführt wird, wenn poll_one() aufgerufen wird. Jedoch, einmal in etwa 300 mal, ist die Ausgabe tatsächlich 3. Basierend auf meinem Verständnis der Boost-Bibliothek scheint dies falsch. Ist das nicht-deterministische Verhalten ein Fehler oder erwartet?io_service :: poll_one nicht-deterministisches Verhalten

#include <boost/asio.hpp> 

int main() { 
    boost::asio::io_service io; 
    boost::asio::io_service::work io_work(io); 
    boost::asio::io_service::strand strand1(io); 
    boost::asio::io_service::strand strand2(io); 
    int val = 0; 

    strand1.post([&val, &strand2]() { 
    val = 1; 
    strand2.post([&val]() { 
     val = 2; 
    }); 
    boost::asio::spawn(strand2, [&val](boost::asio::yield_context yield) { 
     val = 3; 
    }); 
    }); 

    io.poll_one(); 
    std::cout << "Last executed: " << val << std::endl; 

    return 0; 
} 

Mit Boost-Asio 1.60.0.6

+0

Warum werden Sie abgelehnt? Antwort geschätzt –

+0

Natürlich ist es vollständig minimal und überprüfbar und leicht zu kompilieren. Ausnahmen werden nicht ausgelöst. –

+0

Wenn Sie die Anzahl der Fälle von 3 auf 2 reduzieren, wird das Problem nicht angezeigt. –

Antwort

12

Das beobachtete Verhalten gut definiert ist und erwarten, aber man sollte es nicht oft erwarten.

Asio verfügt über einen begrenzten Pool von Strangimplementierungen und die Standardzuweisungsstrategie für Stränge ist Hashing. Wenn eine Hash-Kollision auftritt, verwenden zwei Stränge die gleiche Implementierung. Wenn eine Hash-Kollision auftritt, das beispielsweise auf die folgenden vereinfacht demo:

#include <cassert> 
#include <boost/asio.hpp> 

int main() 
{ 
    boost::asio::io_service io_service; 
    boost::asio::io_service::strand strand1(io_service); 
    // Have strand2 use the same implementation as strand1. 
    boost::asio::io_service::strand strand2(strand1); 

    int value = 0; 
    auto handler1 = [&value, &strand1, &strand2]() { 
    assert(strand1.running_in_this_thread()); 
    assert(strand2.running_in_this_thread()); 
    value = 1; 

    // handler2 is queued into strand and never invoked. 
    auto handler2 = [&value]() { assert(false); }; 
    strand2.post(handler2); 

    // handler3 is immediately executed. 
    auto handler3 = [&value]() { value = 3; }; 
    strand2.dispatch(handler3); 
    assert(value == 3); 
    }; 

    // Enqueue handler1. 
    strand1.post(handler1); 

    // Run the event processing loop, executing handler1. 
    assert(io_service.poll_one() == 1); 
} 

Im obigen Beispiel:

  • io_service.poll_one() führt einen einzelnen ready-Handler (handler1)
  • handler2 aufgerufen wird nie
  • handler3 wird sofort innerhalb strand2.dispatch() aufgerufen, wie strand2.dispatch() wird aus einem Handler woaufgerufen wirdkehrt true

Es gibt verschiedene Details zu dem beobachteten Verhalten beitragen:

  • io_service::poll_one() wird die io_service ‚s Ereignisschleife laufen und ohne blockieren, wird es höchstens ein bereit ausführen zu Run-Handler. Handler, die sofort im Kontext einer dispatch() ausgeführt werden, werden niemals in die io_service eingereiht und unterliegen nicht der poll_one() Grenze des Aufrufs eines einzelnen Handlers.

  • Die boost::asio::spawn(strand, function) Überlastung startet eine stackful Koroutine as-if von strand.dispatch():

    • wenn strand.running_in_this_thread() kehrt false für den Anrufer, dann wird der Koroutine in die strand für latente Aufruf veröffentlicht
    • wenn strand.running_in_this_thread() gibt true für den Aufrufer zurück, dann wird die Coroutine sofort ausgeführt
  • Diskrete strand Objekte, die dieselbe Implementierung verwenden, behalten die Garantien eines Strangs bei. Eine gleichzeitige Ausführung wird nämlich nicht auftreten, und order of handler invocation ist gut definiert. Wenn diskrete strand Objekte diskrete Implementierungen verwenden und mehrere Threads die io_service ausführen, kann man beobachten, dass die diskreten Stränge gleichzeitig ausgeführt werden. Wenn jedoch diskrete strand Objekte die gleiche Implementierung verwenden, wird die Parallelität nicht beobachtet, auch wenn mehrere Threads die io_service ausführen.Dieses Verhalten ist documented:

    Die Implementierung gibt keine Garantie, dass Handler, die über unterschiedliche Strang-Objekte gesendet oder gesendet werden, gleichzeitig aufgerufen werden.

  • Asio verfügt über einen begrenzten Pool von Strangimplementierungen. Der aktuelle Standardwert ist 193 und kann durch Definieren von BOOST_ASIO_STRAND_IMPLEMENTATIONS auf die gewünschte Anzahl gesteuert werden. Diese Funktion ist in der Boost.Asio 1.48 release notes merkt

    , die Anzahl von strang Implementierungen konfigurierbar BOOST_ASIO_STRAND_IMPLEMENTATIONS auf die gewünschte Anzahl zu definieren.

    Durch die Verringerung der Poolgröße erhöht man die Wahrscheinlichkeit, dass zwei getrennte Stränge die gleiche Implementierung verwenden. Wenn mit dem ursprünglichen Code die Poolgröße auf 1 festgelegt werden soll, werden strand1 und strand2 immer die gleiche Implementierung verwenden, was dazu führt, dass val immer 3 (demo) ist.

  • Die Standardstrategie zum Zuweisen von Strangimplementierungen ist die Verwendung eines Golden-Ratio-Hash. Da ein Hashing-Algorithmus verwendet wird, gibt es ein Potenzial für Kollisionen, was dazu führt, dass dieselbe Implementierung für mehrere diskrete strand Objekte verwendet wird. Durch die Definition von BOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION kann die Zuweisungsstrategie in Round-Robin geändert werden, wodurch verhindert wird, dass eine Kollision auftritt, bis BOOST_ASIO_STRAND_IMPLEMENTATIONS + 1 Strangzuweisungen stattgefunden haben. Diese Funktion wird in dem Boost.Asio 1.48 Release Notes festgestellt:

    Zusätzliche Unterstützung für ein neues BOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION Flag, das die Zuordnung von Strang Implementierungen schaltet eher einen Round-Robin-Ansatz zu verwenden, als Hashing.

die vorstehenden Angaben gegeben, geschieht folgendes, wenn 1 im ursprünglichen Code beobachtet wird:

  • strand1 und strand2 diskrete Implementierungen
  • io_service::poll_one() die einzelnen Handler ausführt, die direkt gebucht wurde in strand1
  • der Handler, der ingebucht wurdesetzt val-1
  • der Handler in strand2 gebucht wird die Warteschlange eingereiht und aufgerufen nie
  • die Koroutine Schöpfung aufgeschoben wird, wie strand ‚s Reihenfolge der Aufruf Garantie verhindert, dass die Koroutine aus bis nach dem vorherigen Handler erstellt werden, die gepostet in strand2 ausgeführt hat:

    einen Strang Objekt s gegeben, wenn s.post(a) geschieht-before s.dispatch(b), wobei letzterer außerhalb des Stranges durchgeführt wird, dann geschieht asio_handler_invoke(a1, &a1)-before asio_handler_invoke(b1, &b1).

Auf der anderen Seite wird, wenn 3 beobachtet wird:

  • eine Hash-Kollision auftritt für strand1 und strand2, in ihnen entstehende die unter Verwendung der gleichen zugrundeliegenden Strang Implementierung
  • io_service::poll_one() ausführt einzelner Handler, der direkt in strand1
  • der Handler gebucht wurde, der in 012 gebucht wurdeSätze val-1
  • der Handler in strand2 gebucht wird die Warteschlange eingereiht und aufgerufen nie
  • die Koroutine sofort erstellt und innerhalb boost::asio::spawn() aufgerufen, val-3 Einstellung, wie strand2 sicher die Koroutine ausführen kann, während die Garantie nicht beibehalten gleichzeitige Ausführung und Reihenfolge des Handleraufrufs
+4

Diese Antwort ist ein Kunstwerk. Ich liebte die kleinen Demos der Randfälle. +100 AFAIAC – sehe

+0

Tolle Antwort. Dies wäre eine wertvolle Information in der Dokumentation der Strangklasse. – hifier

+0

@sehe Danke! Ihr Engagement, über die Jahre hinweg herausragende Antworten in allen SO-Ländern zu liefern, war eine enorme Motivation für mich. Diese Antwort wäre ohne dich nicht hier. –

Verwandte Themen