Asynchrone Sachen mit Bordmitteln sicher beenden.
-
Hallo, ich hatte schon im Winprog-Forum mal etwas in die Richtung geschrieben, aber da ich nun boost, mutex und condition_variable benutze, finde ich, dass es auch im C++-Forum ganz gut aufgehoben ist.
Ich habe mir folgende asio-Timer-Endlos-Intervall-Klasse geschrieben:
class AsioIntervalTimerImpl { public: AsioIntervalTimerImpl( asynch::AsioPool& pool, std::function<void()> handler, double interval ) : timer_( pool.get_service() ), handler_( handler ), interval_( interval ), running_(false), cancelling_(false) { Start(); } ~AsioIntervalTimerImpl() { Stop(); // :( } void Stop() { try { std::unique_lock<std::mutex> guard( mutex_ ); cancelling_ = true; std::size_t num_canceled = timer_.cancel(); // So, an dieser Stelle könnte // a) Nichts mehr los sein, weil der Timer nicht lief (num_canceled=0) // b) Eine Operation mit err=0 auf dem Weg zur Ausführung sein, weil sie das schon war, während oder bevor man cancel aufgerufen hat. (num_canceled=0) // c) Eine Operation mit err!=0 auf dem Weg zur Ausführung sein, weil der Timer lief, aber im Wartezustand war. (num_canceled=1) // => man könnte sich den Rückgabewert auch sparen. if( running_ ) { cancelled_condition_.wait( guard ); } assert( running_ == false ); } catch( boost::system::system_error& err ) { std::cout << std::string("Error while stopping interval timer: ") + err.what() << std::endl; } } private: void Start() { try { timer_.expires_from_now( boost::posix_time::millisec(interval_*1000) ); timer_.async_wait( std::bind( &AsioIntervalTimerImpl::TimerHandler, this, std::placeholders::_1 ) ); running_ = true; } catch( boost::system::system_error& err ) { std::cout << std::string("Error while creating and starting interval timer: ") + err.what() << std::endl; running_ = false; throw; } } void TimerHandler( const boost::system::error_code& error ) { assert( running_ == true ); std::unique_lock<std::mutex> guard( mutex_ ); if( cancelling_ ) { running_ = false; cancelled_condition_.notify_all(); } else { if( !error ) { handler_(); Start(); } else { running_ = false; } } } bool cancelling_; bool running_; std::condition_variable cancelled_condition_; std::mutex mutex_; double interval_; std::function< void() > handler_; boost::asio::deadline_timer timer_; };Also wie man sieht, musste ich eine condition_variable und 2 flags einfügen, um irgendwie die Garantie zu machen, dass TimerHandler nicht mehr aufgerufen wird, wenn die Stop-Methode durchgelaufen ist.
Jetzt meine Fragen:- und allerwichtigst: Habe ich noch irgendeinen Fall übersehen? Wäre unschön, wenn die Garantie nicht eingehalten würde.
- Geht diese Fallbehandlung das nicht auch irgendwie sagen wir einmal übersichtlicher oder auch methodischer?
Viele Grüße,
DeciEdit: Ist das nicht irgendwie so eine Form von Handshake zwischen den beiden Threads? Vielleicht sollte ich das mal in eine eigene Klasse "abstrahieren".
-
Was genau macht denn asynch::AsioPool? Und warum boost::asio::deadline_timer über io_service? Macht die Klasse irgendwie noch mehr als handler in einem bestimmten Interval aufzurufen?
-
asynch::AsioPool ist ein Threadpool um einen io_service. Also dasselbe, was win32 mit den IO-Completion Ports und seinen Threadpools, die boost ja auch verwendet, anbietet, in C++ und ohne sich von Windows abhängig zu machen.
Der deadline_timer sorgt für - diese Klasse betreffend - regelmäßige events, was während dieser Events geschieht, weiß er nicht. Diese Events müssen asynchron abgearbeitet werden, weil sie nicht durch einen vollen GUI-Thread (der gerade lustig vor sich hinzeichnet) verzögert werden sollen (Und Timer genießen mindestens mal unter Windows allerniedrigste Priorität).
Genauso muss ich zeitig auf externe io events reagieren, das läuft über denselben Pool mit denselben Mitteln.
Ich möchte nicht den Eindruck erwecken, dass es mir auf Millisekunden-genaue Verarbeitung ankommt, aber mehrere Sekunden brachliegende Verarbeitung weil jemand am Fenster zieht oder das Zeichnen mal länger dauert, ist nicht drin - und das war der Zustand davor.Wollte ich nur als die Beweggründe für IO und Timer über den io_service in Kombination mit dem Pool erwähnen.
-
Ich verstehe halt noch nicht ganz, warum man das koppeln sollte. Wenn du alle x Sekunden etwas ausführen willst, warum machst du dir dann nicht einfach selbst einen Thread?
-
Weil, wenn ich für alles, das ich hier habe und asynchron abläuft, ich dann bei 20 Threads wäre (und das ist alles nicht zur Compile-Zeit fix), bei denen 95% zur gleichen Zeit brachliegen und die Objekte dennoch Threadsafe sein müssen, weil sie über mehrere asynchrone Pfade aufgerufen werden. Zudem erhöhe ich so die Anzahl der Cores und damit den Durchsatz (hat natürlich auch die Grenze, wenn ein Core nicht mehr damit zurande kommt, die Events zu verteilen, aber das ist eh alles nur hypothetisch).
Also anders gefragt: Was ist schlecht daran, wie ich es mache. Bei etwaigen Vorteilen anderer Ansätze freue ich mich ja natürlich. Mir kam das nur all der "the right thing to do"-Ansatz vor.
-
20 Threads sind doch nicht viel. Jedenfalls nicht, wenn die meisten einfach nur blocken. Aufgaben die einfach nur blocken "verbrauchen" in einem Thread-Pool letztlich auch einfach nur einen Thread, wenn du das verhindern willst, musst du schon etwas schlaueres basteln. Eventuell nutzt asio allerdings einen Interval Mechanismus des Systems, das weiß ich nicht. Wie auch immer, wenn du nur 5 oder 10 solcher Dinger hast, die einfach fest "ticken", würde ich mir darum keine Gedanken machen, und einfach so etwas nutzen: (Schnell zusammen gefrickelt!)
class interval_timer { public: typedef std::chrono::nanoseconds interval_type; private: std::atomic_bool stop_; std::mutex m_; std::condition_variable cv_; std::thread thread_; public: template <typename H> interval_timer(H handler, interval_type interval) // Auskommentiert, da VS die atomic_bool initialisierung noch nicht mag. //: stop_(false) //, thread_([this, handler, interval] { //std::unique_lock<std::mutex> lock(m_); // while (cv_.wait_for(lock, interval) == std::cv_status::timeout) // { // handler(); // if (stop_) // break; // } //}) { stop_ = false; thread_ = std::thread([this, handler, interval] { std::unique_lock<std::mutex> lock(m_); while (cv_.wait_for(lock, interval) == std::cv_status::timeout) { // Wenn ein schnelles Abbrechen nicht nötig ist, kann man auch gleich // auf die condition_variable verzichten, dann wird's richtig leicht. handler(); if (stop_) break; } }); } ~interval_timer() { stop_ = true; cv_.notify_one(); } void stop() { stop_ = true; cv_.notify_one(); thread_.join(); } }; int main() { interval_timer timer([] { std::cout << "Tick!\n"; }, std::chrono::seconds(1)); std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); timer.stop(); std::cout << "Tock!\n"; }Wenn diese Threads dann allerdings mit irgendetwas von außen skalieren (Nutzer, Eingaben, ...), würde ich es definitiv anders machen. Trotzdem bin ich mir nicht so sicher, ob der asio Thread-Pool da das Richtige ist. Aber da müsste ich dann die Umstände genauer kennen.
-
Der Aufbau, das Vorhandensein, die Verschaltung und die Anzahl der Objekte ist vom Benutzer abhängig. Ich hätte dann die Wahl zwischen einem Thread der irgendwann umfällt und sich selbst blockiert oder einem Thread pro asynchronen Objekt (und ständigen Thread-wechseln).
So habe ich den Vorteil, dass die Events nach Freiheit verteilt werden und ein Thread sich direkt neue Arbeit besorgen kann, wenn er mit der letzten fertig ist, ohne einen weiteren Threadwechsel durchführen zu müssen. Ich meine, diese IO-Completion-Port-Sache skaliert extrem gut, aber ich bin auch nicht der Profi, der auf dieser Meinung beharren würde.PS: Ich lese noch (und werde morgen frühe weiter) deinen Code. Ich habe noch das Problem, dass es bei mir dauert, bis ich beurteilen kann, ob ein sich synchronisierender Code so korrekt ist (Mir bleibt im Moment nicht mehr, als die Fälle durchzuspielen, weil ich nicht auf Muster zurückgreifen kann...).
-
Okay, alles klar, morgens ist immer besser als spät in der Nacht.
Eine Frage: Warum atomic_bool anstatt eines locks im Destruktor? Ich stelle mir das gerade so vor, dass bei jedem read in der Threadfunktion eine "atomic read" stattfinden muss, wieviel auch immer der kostet oder wie das in der CPU umgesetzt ist, obwohl sowieso schon ein Lock auf die Ressource gehalten wird.Viele Grüße und Danke
für das Mitmachendafür, dass Du Dir für mich die Zeit nimmst!
Deci
-
atomic Variablen können mitunter deutlich effizienter sein als locks. In jedem Fall sind sie "simpler" insofern, warum sollte man sie nicht benutzen?

Completion Ports sind sicher effizient, auch wenn ich noch nie gesehen habe, dass jemand sie für Interval-Timeouts genutzt hat. Mein Anliegen ist halt eher, dass ich mir nicht so sicher bin, ob sich diese Kopplung (diese Timeout Klasse ist ja jetzt nur in dieser Konfiguration mit asio benutzbar) wirklich lohnt, wenn man es auch mit Boardmitteln machen könnte.
Aber bitte, wenn hier ein Timeout/User o.Ä. erstellt wird, nicht die Lösung von oben benutzen! Ein Thread/User skaliert leider nur bis etwa 1000 Nutzern, u.U. noch schlechter. Aber dafür müsste ich halt noch ein paar mehr Dinge wissen: Wie genau muss das Timeout sein? (Die Zeit, die handler() benötigt, wird hier ja nicht mit einbezogen.) Haben alle Handler bei dir das gleiche Interval? Könnte man also eine Klasse nehmen die mehrere Handler, aber nur ein Interval hält?
-
cooky451 schrieb:
Ein Thread/User skaliert leider nur bis etwa 1000 Nutzern, u.U. noch schlechter.
Stimmt doch gar nicht.
-
Hallo cooky,
nun ich habe mich vielleicht etwas unglücklich ausgedrückt. Ich bin überzeugt davon, dass du mit den Kosten von atomic vs. mutex Recht hast. Ich meine viel eher, dass man diese atomic-Kosten bei Deinem Ansatz jeden Timer-Zyklus hat, vs. einmal die Mutex-Kosten in der stop-Methode.
Ich benutze die completion-ports auch hauptsächlich deshalb, weil ich sie schon für das asynchrone I/O benutze, also Dateien, serielle Schnitstellen und Netzwerkverkehr. Da habe ich die Timer einfach gleich mit einbezogen. Bei endlos-Lese-Sachen habe ich genau dasselbe Problem wie im OP beschrieben, nur ließ sich der Timer-Code einfacher als Beispiel hernehmen, bei dem anderen I/O hätte ich erst zusammenstauchen müssen!Bis denne,
Deci
-
@ Volkard,
kannst du darauf etwas näher eingehen?
Ich glaube, die Diskussion kränkelt gerade daran, dass ich weder die 1-Thead/Asynchroner Komminikation-Strategie noch die Completion-Port-Strategie bis zum Kollaps ausprobiert habe, um Vergleiche zu führen zu können und Cooky keine Erfahrungswerte mit den Completion-Ports gesammelt hat.
Meine Wahl fiel auf sie, da sie mir einfach als "passend" einleuchteten, als ich mir über die Vorgehensweise Gedanken gemacht habe. Es funktioniert auch alles wunderbar, daher müsste ich mir erstmal Gründe erarbeiten, warum ich einen anderen Weg einschlagen sollte. Das ist schließlich immer mit Arbeit verbunden, später natürlich mit mehr als früher, daher bin ich auch sehr stark interessiert an einer vernünftigen Abwägung.Viele Grüße,
Deci
-
Du hast vergessen etwas zu beantworten:
cooky451 schrieb:
Aber dafür müsste ich halt noch ein paar mehr Dinge wissen: Wie genau muss das Timeout sein? (Die Zeit, die handler() benötigt, wird hier ja nicht mit einbezogen.) Haben alle Handler bei dir das gleiche Interval? Könnte man also eine Klasse nehmen die mehrere Handler, aber nur ein Interval hält?

-
Also ich möchte die durchaus bis runter zu den ~15ms verwenden können (aber mindestens irgendwas bei den 60ms), die Windows nunmal maximal bietet, bei Linux erreicht man so wohl 10ms? Keine Ahnung. Und ja, es kommen unterschiedliche Intervallzeiten zum Zuge. Und klar wackelt dann die ganze Zeit auch noch in der Phase oder wie man das ausdrücken soll ("Jitter"?). Was ich auf dem Bildschirm sehe, macht auf mich aber einen zufriedenstellenden Eindruck, also ist das Timing, so wie es gerade läuft, "gut genug".
Im GUI-Thread lege ich alle getimeten Animationen mit gleichem Intervall, und das sind wohl im Moment wohl alle, einfach adaptiv auf einen Timer zusammen (Das ist auch nötig, weil sonst längere Zeichenoperationen die restliche Animationen verdrängen). Da das beim asynchronen Prozess aber nicht so garantiert (weil Laufzeitkonfigurationsabhängig) ist, habe ich mir noch keine Mühe gemacht, asynchrone Timer "adaptiv" zusammenzulegen.Wirkliche Echtzeit-Anforderungen habe ich aber nicht, nur so Pi mal Daumen so genau wie möglich.
Edit: Zweiter Grund warum ich die Timer nicht zusammengelegt habe: Das kann man auch in der Konfiguration so zusammenclicken, wenn man es denn will, spart erstmal Arbeit ;).
-
Nun gut,
ich habe etwas rumrecherchiert und es scheint so, dass die 1 Thread/Sache-Sache doch das Optimum darstellen könnte. Ich habe aber noch keine weitergehenden Vergleiche gesehen, die das im Vergleich mit den Completion Ports setzen. Auf jeden Fall behalte ich das einmal im Hinterkopf.Gibt es eigentlich unter Windows noch irgendeine Option, die Timinggeschichte über etwas anderes als die herkömmlichen Timer zu regeln? Wie machen das denn Multimedia-Anwendungen (Media-Player, VLC, name it) Wohl eher so selbst steuerbar nicht, weil die mit dem Scheduler-Intervall zusammenhängen, oder?
Ich hatte in dem Zusammenhang auch mal über RTX-Extensions gelesen, bei denen man einzelne Cores komplett für Echtzeit-Sachen übernehmen kann, wenn das keine Knete kosten würde, wäre das echt eine tolle Geschichte
Ansonsten bleibt einem ja wahrscheinlich "nur" irgendeine angepasste Version von Linux zu verwenden, und ich bin kein Linux-Typ 
Für meine derzeitige Arbeit ist das nicht wichtig (sprich total unwichtig), aber ich träume noch davon, Echtzeitmodellsimulation für meine Roboterregelung umzusetzen, da wäre sowas echt gut. Wobei man sich natürlich auch mit einem Puffer und etwas Latenz behelfen kann. Aber der "Mikrocontroller"-Ansatz ist halt echt simpel.
-
Decimad schrieb:
@ Volkard,
kannst du darauf etwas näher eingehen?Die Rechner sind vieeel schneller als ihr Ruf.
Ich lasse gerade einen vecor sortieren, der 80G groß ist bei nur 16G Ram. Na, und? Das BS läuft auf einem USB-Stick, 120G SSD und 1800G Platte darf er zum Swappen benutzen.
Wenn er eine Million Threads pro Sekunde erzeugen kann, ist das nicht ausreichend? Und wenn er bei 100000 gleichzeitig laufenden Threads ein wenig in die Knie geht, kauft man sich eben was cooles. Zweiten Rechner oder sowas.
Allerdings würde ich am Speicherverbrauch sehr knauserig sein. Zum Beispiel für einfache Webserverdinge jedem Thread nur 64k Stack geben, wo er auch noch seinen Freispeicher reinlegen soll.
Bevor ich Texten von vor 10 Jahren glauben würde (die übrigens ihrerseits älteres Wissen replizierten), würde ich ein Weilchen selber messen. Mir fehlen immer die Begründungen bei den 1:1-Ablehnern. Wo hakt es? Sind die Mutexe bei so vielen Anhängern lahm? Geht der Speicher aus? Wird der Scheduler lahm?
Wobei ich aber auch davon ausgehe, mir im Zweifelsfall das Betriebssystem aussuchen zu können. Vielleicht wird bei Windows irgendein nutzloser Sicherheitsdienst überlastet, vielleicht wird bei Linux das Speicher-Allokieren lahm, vielleicht sollte Dragonfly-BSD benutzt werden.
-
Decimad schrieb:
Gibt es eigentlich unter Windows noch irgendeine Option, die Timinggeschichte über etwas anderes als die herkömmlichen Timer zu regeln? Wie machen das denn Multimedia-Anwendungen (Media-Player, VLC, name it)
Du wirst es nicht glauben, extra dafür hat Windows Multimedia-Timer.
Die Waitable Timers http://msdn.microsoft.com/en-us/library/windows/desktop/ms686289(v=vs.85).aspx haben aber auch eine ganz hübsche Auflösung. Ich habe aber noch nicht gemessen, wie zweigerecht die Threads geweckt werden.
-
Whoah, Danke, die werde ich morgen mal im 1 Thread/Timer-Modus ausprobieren!
Das gute ist, dass die einzelnen Entitäten sowieso Threadsafe sein müssen, daher ist es rille, wie ich die Aufteilung der Events veranstalte.
Leider bin ich gerade erstmal an einer anderen Baustelle... Die Arbeit will nicht nachlassen...