2016-10-27 7 views
14

Wir verwenden seit Jahren asio in der Produktion und vor kurzem haben wir einen kritischen Punkt erreicht, wenn unsere Server gerade genug geladen werden, um ein mysteriöses Problem zu bemerken.boost :: asio Argumentation hinter num_implementations für io_service :: strang

In unserer Architektur verwendet jede separate Entität, die unabhängig ausgeführt wird, ein persönliches Objekt strand. Einige der Entitäten können eine lange Arbeit ausführen (Lesen von einer Datei, Ausführen einer MySQL-Anfrage, usw.). Offensichtlich wird die Arbeit in Handlern ausgeführt, die mit einem Strang umwickelt sind. Alles hört sich gut und schön an und sollte einwandfrei funktionieren, bis wir bemerken, dass unmögliche Dinge, wie Timer, Sekunden später auslaufen, obwohl Threads "auf Arbeit warten" und ohne erkennbaren Grund arbeiten. Es sah so aus, als hätte lange Arbeit in einem Strang Auswirkungen auf andere nicht verwandte Stränge gehabt, nicht auf alle, sondern auf die meisten.

Unzählige Stunden wurden verbracht, um das Problem zu lokalisieren. Die Spur hat zu dem Weg geführt, strand Objekt wird erstellt: strand_service::construct (here).

Aus irgendeinem Grund entschieden sich Entwickler für eine begrenzte Anzahl von strand Implementierungen. Das bedeutet, dass einige Objekte, die keine Beziehung zueinander haben, eine einzige Implementierung gemeinsam nutzen und daher Engpässe aufweisen.

In der eigenständigen (nicht Boost) Asio Bibliothek ähnlicher Ansatz wird verwendet. Aber anstelle von geteilten Implementierungen ist jede Implementierung nun unabhängig, kann jedoch ein mutex Objekt mit anderen Implementierungen teilen (here).

Worum geht es? Ich habe noch nie von einer Begrenzung der Anzahl der Mutexe im System gehört. Oder irgendwelche Gemeinkosten in Bezug auf ihre Schaffung/Zerstörung. Obwohl das letzte Problem leicht gelöst werden könnte, indem man Mutexe recycelt, anstatt sie zu zerstören.

Ich habe einen einfachen Testfall um zu zeigen, wie dramatisch eine Leistungsverschlechterung ist:

#include <boost/asio.hpp> 
#include <atomic> 
#include <functional> 
#include <iostream> 
#include <thread> 

std::atomic<bool> running{true}; 
std::atomic<int> counter{0}; 

struct Work 
{ 
    Work(boost::asio::io_service & io_service) 
     : _strand(io_service) 
    { } 

    static void start_the_work(boost::asio::io_service & io_service) 
    { 
     std::shared_ptr<Work> _this(new Work(io_service)); 

     _this->_strand.get_io_service().post(_this->_strand.wrap(std::bind(do_the_work, _this))); 
    } 

    static void do_the_work(std::shared_ptr<Work> _this) 
    { 
     counter.fetch_add(1, std::memory_order_relaxed); 

     if (running.load(std::memory_order_relaxed)) { 
      start_the_work(_this->_strand.get_io_service()); 
     } 
    } 

    boost::asio::strand _strand; 
}; 

struct BlockingWork 
{ 
    BlockingWork(boost::asio::io_service & io_service) 
     : _strand(io_service) 
    { } 

    static void start_the_work(boost::asio::io_service & io_service) 
    { 
     std::shared_ptr<BlockingWork> _this(new BlockingWork(io_service)); 

     _this->_strand.get_io_service().post(_this->_strand.wrap(std::bind(do_the_work, _this))); 
    } 

    static void do_the_work(std::shared_ptr<BlockingWork> _this) 
    { 
     sleep(5); 
    } 

    boost::asio::strand _strand; 
}; 


int main(int argc, char ** argv) 
{ 
    boost::asio::io_service io_service; 
    std::unique_ptr<boost::asio::io_service::work> work{new boost::asio::io_service::work(io_service)}; 

    for (std::size_t i = 0; i < 8; ++i) { 
     Work::start_the_work(io_service); 
    } 

    std::vector<std::thread> workers; 

    for (std::size_t i = 0; i < 8; ++i) { 
     workers.push_back(std::thread([&io_service] { 
      io_service.run(); 
     })); 
    } 

    if (argc > 1) { 
     std::cout << "Spawning a blocking work" << std::endl; 
     workers.push_back(std::thread([&io_service] { 
      io_service.run(); 
     })); 
     BlockingWork::start_the_work(io_service); 
    } 

    sleep(5); 
    running = false; 
    work.reset(); 

    for (auto && worker : workers) { 
     worker.join(); 
    } 

    std::cout << "Work performed:" << counter.load() << std::endl; 
    return 0; 
} 

Bauen diesen Befehl:

g++ -o asio_strand_test_case -pthread -I/usr/include -std=c++11 asio_strand_test_case.cpp -lboost_system 

Testlauf in üblichen Weise:

time ./asio_strand_test_case 
Work performed:6905372 

real 0m5.027s 
user 0m24.688s 
sys  0m12.796s 

Testlauf mit einer langen Blockierungsarbeit:

time ./asio_strand_test_case 1 
Spawning a blocking work 
Work performed:770 

real 0m5.031s 
user 0m0.044s 
sys  0m0.004s 

Der Unterschied ist dramatisch. Was passiert, ist, dass jede neue nicht blockierende Arbeit ein neues Objekt strand erstellt, bis es die gleiche Implementierung mit strand der Blockierungsarbeit teilt. Wenn das passiert, ist es eine Sackgasse, bis die Arbeit beendet ist.

bearbeiten: Reduzierte parallele Arbeiten bis auf die Anzahl der Arbeitsfäden (1000-8) und aktualisierte Testlauf ausgegeben. Ist dies der Fall, weil, wenn beide Zahlen in der Nähe sind, das Problem besser sichtbar ist.

Antwort

3

Nun, ein interessantes Thema und +1 für ein kleines Beispiel, das genau das Problem reproduziert.

Das Problem, das Sie "wie ich verstehe" mit der Boost-Implementierung ist, dass es standardmäßig nur eine begrenzte Anzahl von strand_impl, 193 instanziiert, wie ich in meiner Version von Boost (1.59) sehe.

Nun bedeutet dies, dass eine große Anzahl von Anforderungen in Konflikt geraten, da sie darauf warten würden, dass die Sperre durch den anderen Handler entsperrt wird (mit derselben Instanz von strand_impl).

Meine Vermutung, so etwas zu tun, würde sein, das OS zu überlasten, indem man viele, viele und viele Mutexe erstellt. Das wäre schlecht. Die aktuelle Implementierung ermöglicht es, die Sperren wieder zu verwenden (und in einer konfigurierbaren Weise, wie wir weiter unten sehen werden)

In meinem Setup:

 
MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -O2 -o strand_issue strand_issue.cc -lboost_system -pthread 
MacBook-Pro:asio_test amuralid$ time ./strand_issue 
Work performed:489696 

real 0m5.016s 
user 0m1.620s 
sys 0m4.069s 
MacBook-Pro:asio_test amuralid$ time ./strand_issue 1 
Spawning a blocking work 
Work performed:188480 

real 0m5.031s 
user 0m0.611s 
sys 0m1.495s 

Jetzt gibt es eine Möglichkeit, zu ändern, um diese Anzahl von zwischengespeicherten Implementierungen Einstellen des Makros BOOST_ASIO_STRAND_IMPLEMENTATIONS.

unten ist das Ergebnis, das ich nach bekam er auf einen Wert von 1024 einstellen:

 
MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -DBOOST_ASIO_STRAND_IMPLEMENTATIONS=1024 -o strand_issue strand_issue.cc -lboost_system -pthread 
MacBook-Pro:asio_test amuralid$ time ./strand_issue 
Work performed:450928 

real 0m5.017s 
user 0m2.708s 
sys 0m3.902s 
MacBook-Pro:asio_test amuralid$ time ./strand_issue 1 
Spawning a blocking work 
Work performed:458603 

real 0m5.027s 
user 0m2.611s 
sys 0m3.902s 

Fast die gleiche für beide Fälle! Möglicherweise möchten Sie den Wert des Makros gemäß Ihren Anforderungen anpassen, um die Abweichung gering zu halten.

+1

* "Meine Vermutung für solch eine Sache wäre, das OS nicht zu überlasten, indem man viele, viele und viele Mutexe erstellt. Das wäre schlecht." * Warum? Welcher Overhead gibt es abgesehen von kleinen konstanten (pro Mutex) Betragsspeicher? –

+1

@yurikilochek Sie sind Mutexe. Per Definition sind sie nutzlos, wenn sie nicht zum Synchronisieren verwendet werden. Dadurch werden große Sammlungen von Synchronisationsgrundelementen gleichzeitig abgewartet. ':: WaitForMultipleObjectsEx' stört vielleicht nicht, aber das ist ein Kontextwechsel, es sind nicht nur ein paar Bytes Speicher. Unter Linux gibt es keinen solchen Aufruf AFAIK. – sehe

+0

@Arunmu Unabhängig von der Anzahl der Implementierungen wird das Problem bestehen bleiben, da es im Design ist. Das Erhöhen der Anzahl kann einige Zeit gewinnen, aber nur in gewissem Maße. In Echtzeit wird dies nie funktionieren. Probieren Sie mein Beispiel mit 'work objects' gleich der Anzahl der Threads, also' 8' statt '1000'. In diesem Fall helfen '1024' Implementierungen kaum (' Work performed: 8331'). – GreenScape

2

Standalone ASIO und Boost.ASIO haben sich in den letzten Jahren ziemlich abgekapselt, da das eigenständige ASIO langsam in die Referenz-Netzwerk-TS-Implementierung für die Standardisierung umgewandelt wird. Die ganze "Aktion" findet im eigenständigen ASIO statt, einschließlich großer Bugfixes. Nur sehr kleine Fehlerbehebungen wurden an Boost.ASIO vorgenommen. Zwischen ihnen gibt es inzwischen mehrere Jahre.

Ich würde daher vorschlagen, dass jeder überhaupt Probleme mit Boost finden. ASIO sollte auf Standalone ASIO umstellen. Die Konvertierung ist normalerweise nicht schwer, schauen Sie sich die vielen Makrokonfigurationen an, um in config.hpp zwischen C++ 11 und Boost zu wechseln. Historisch gesehen wurde Boost.ASIO tatsächlich automatisch vom Skript aus Standalone-ASIO generiert, es kann der Fall sein, dass Chris diese Skripte funktionierte, und so konnten Sie einen brandneuen Boost.ASIO mit den neuesten Änderungen neu generieren. Ich würde vermuten, dass ein solcher Build jedoch nicht gut getestet ist.

+0

Das ist interessant @Niall Douglas. Mit Blick auf die Release Notes wurde die letzte Version von [standalone asio] (http://think-async.com/) in [boost] (http://www.boost.org/users/news/) geschafft zurück im April 2015. Diese Version war asio 1.10.6 während der letzten [asio Entwicklungsversion] (http://think-async.com/asio/asio-1.11.0/doc/asio/history.html#asio.history .asio_1_11_0) zeigt 1.10.5 als letzte Hauptversion, also hast du recht, sie haben sich auseinandergerissen, während Chris sich auf den Vorschlag für die Netzwerkbibliothek konzentriert, jetzt [N4612] (http://open-std.org/JTC1/ SC22/WG21/docs/papers/2016/n4612.pdf) – kenba

+0

Leider wurde die 'strang_impl' Allokationsstrategie in der Standalone-Version nicht geändert. Auf dem 'strang_executor_service' sind einige Arbeiten in die richtige Richtung. Ich habe versucht, es in Vanille 'strang_service' zu ​​portieren, aber ohne Glück. Derzeitiges Design hängt so sehr von der Garantie ab, dass "strand_impl" nicht zerstört wird, Ereignis nach "strand" ist, dass es fast unmöglich ist, ohne Redesign zu reparieren. Auf jeden Fall habe ich an die Mailingliste geschrieben. – GreenScape

Verwandte Themen