Meyers Singleton auf dem Heap
-
Ja das geh so. Jede Initialisierung von Funktions-lokalen statics ist ab C++11 thread-safe. Alles andere natürlich nicht. Wenn also mehrere Threads dieses Objekt benutzen wollen, müssen die sich natürlich schon koordinieren. Du solltest also einen mutex als Datenelement mit aufnehmen und diesen fleißig "locken".
Und wenn so ein Singleton auch irgendwann ordentlich zerstört werden soll, würde ich das mit shared_ptr statt rohen Zeigern probieren. shared_ptr kommt auch mit mehreren Threads klar, was den internen Referenzzähler angeht.
-
TyRoXx schrieb:
Wofür braucht man denn ein Singleton? Eine globale Variable für Logging oder so kann ich ja verstehen. [...] Ich habe aber noch nie eine sinnvolle Implementierung eines globalen Objektes mit Initialisierung bei Bedarf gesehen.
Genau darum gehts, Singletons z.b. für den Logger, oder den GlobalEventManager anstatt globaler Variablen. Der sinn ist schlicht und einfach das “static initialization order fiasco” zu vermeiden. Zudem gibts keine main bei mir, ich schreibe ein lib die, falls ich sie jemals veröffentliche, den User nicht zwingen soll meinen Logger, oder meinen Eventmanager zu verwenden, und falls doch braucht er sich um die Initialisierung nicht kümmern.
Vielleicht haette ich dazu schreiben sollen das es mir Hauptsächlich um das Verständnis geht was genau passiert. Ich hab die letzten Jahre privat praktisch nur in dynamische Sprachen (hauptsächlich Lisp/Clojure) programmiert. Beruflich habe in der Automatisierungs-/Virtualisierungsbranche programmiert, dynamische Speicherverwaltung gibts dort schlichtweg nicht (zumindest ist mir keine SPS/Visualierungssoftware bekannt die es unterstützt). Desswegen muss ich mich erstmal wieder in die systemnahe Programmierung einfinden.
krümelkacker schrieb:
Ja das geh so. Jede Initialisierung von Funktions-lokalen statics ist ab C++11 thread-safe.
Also Explit auch durch Pointer referenzierte Objekt?
krümelkacker schrieb:
Alles andere natürlich nicht. Wenn also mehrere Threads dieses Objekt benutzen wollen, müssen die sich natürlich schon koordinieren. Du solltest also einen mutex als Datenelement mit aufnehmen und diesen fleißig "locken".
Ich verwende in meinem Projekt keine Mutexe und nach möglichkeit keine locks. Locking allgemein ist mir zu teuer und vor allem unsicher (Locks sind fuer mich der Teufel in Person), ich finds aber auch unelegant. Nachdem ich jetzt etliche Jahre Clojure programmiert habe, bin ich eher ein Fan von async, verwende aber hauptsächlich die Relaxed Semantik (wenn schon C++ warum dann nicht richtig auf performance setzen). Ich bin aber schon gepannt auf das mit C++17 kommende STM.
krümelkacker schrieb:
Und wenn so ein Singleton auch irgendwann ordentlich zerstört werden soll, würde ich das mit shared_ptr statt rohen Zeigern probieren. shared_ptr kommt auch mit mehreren Threads klar, was den internen Referenzzähler angeht.
Das mit dem shared_ptr is mir dann auch klar geworden

Schade nur das sie relativ teuer sind.
-
seelenquell schrieb:
oder den GlobalEventManager anstatt globaler Variablen.
Was ist denn ein
GlobalEventManager? Das hört sich nach einer Klasse an, die erstens nicht existieren sollte und zweitens erst recht nicht global.seelenquell schrieb:
Der sinn ist schlicht und einfach das “static initialization order fiasco” zu vermeiden.
Das vermeide ich, indem ich nichts vor der
mainmache, außer mal einenstd::string constmit einem Literal initialisieren oder so.seelenquell schrieb:
Zudem gibts keine main bei mir, ich schreibe ein lib die, falls ich sie jemals veröffentliche, den User nicht zwingen soll meinen Logger, oder meinen Eventmanager zu verwenden, und falls doch braucht er sich um die Initialisierung nicht kümmern.
Ich sehe nicht, was das mit meiner Frage zu tun hat. Um eine globale Variable ohne Singleton muss man sich auch nicht unbedingt kümmern.
-
TyRoXx schrieb:
seelenquell schrieb:
oder den GlobalEventManager anstatt globaler Variablen.
Was ist denn ein
GlobalEventManager? Das hört sich nach einer Klasse an, die erstens nicht existieren sollte und zweitens erst recht nicht global.Evtl ein Bus zwischen Micro-Services? Könnte mir den schon global vorstellen. Ok, die Micro-Services nicht gleich auf die harte Tour, daß jede Schnittstelle serialisierbar ist und jeder Service in jedem Land laufen kann, aber vielleicht trotz monolithischer Anwendung innendrin über den
GlobalEventManagerdie Entwickleteams mächtig entkoppelt. Diese Ebene der Entkopplung geistert mir seit zwei Wochen durch den Kopf für das verworrende Riesenprogramm, das mein Arbeitgeber seit 35 Jahren nicht aufgeräumt hat; wohl fühle ich mich dabei noch nicht.
-
TyRoXx schrieb:
seelenquell schrieb:
oder den GlobalEventManager anstatt globaler Variablen.
Was ist denn ein
GlobalEventManager? Das hört sich nach einer Klasse an, die erstens nicht existieren sollte und zweitens erst recht nicht global.Warum sollte es den keinen GlobalEventManager geben?
Immer diese streitfragen ob etwas global sein darf oder nicht, siehe unten was damit bezweckt ist.

volkard schrieb:
Evtl ein Bus zwischen Micro-Services? Könnte mir den schon global vorstellen. Ok, die Micro-Services nicht gleich auf die harte Tour, daß jede Schnittstelle serialisierbar ist und jeder Service in jedem Land laufen kann, aber vielleicht trotz monolithischer Anwendung innendrin über den
GlobalEventManagerdie Entwickleteams mächtig entkoppelt. Diese Ebene der Entkopplung geistert mir seit zwei Wochen durch den Kopf für das verworrende Riesenprogramm, das mein Arbeitgeber seit 35 Jahren nicht aufgeräumt hat; wohl fühle ich mich dabei noch nicht.Fast richtig

Naja, eigentlich hat mich nur nen Freund gefragt ob ich (just for fun) eine Spieleengine mit ihm schreiben will. Allerdings ist dein Tip find ich garnicht soweit weg.Ich hab mehrere Sub-Systeme (z.B. ein ECS, ein Positioning System, Fight Sytem etc.) die alle auch als unabhängige libs nutzbar sein sollen. Genauer gesagt ist sogar alles so ausgelegt das die Systeme selbst überhaupt nichts eigenständig tun (können), alles läuft asynchron über einen Event Manager. Theoretisch spielts damit auch keine Rolle ob die Systeme auf dem selben PC laufen oder am anderen Ende der Welt. Genau das ist auch das Ziel, alles soweit voneinander zu trennen das genau das möglich wäre.
Ich hab das ganze ausserdem in Realms organisiert, jeder Realm stellt quasi eine eigene Welt dar mit jeweils eigenen Systemen. Ob diese fuer den Realm erstellt werden, oder sich mehrere Realms ein System teilen kann man frei konfigurieren.
Der GlobalEventManager ist im Prinzip auch wieder nur ein normaler Event Manager, nur das er eben die Hoheit über alles hat. So läuft z.b. die komunikation zwischen den Realms standardmäßig über den GEM, er kann andere Eventmanager beenden, Realms zerstören und erschaffen. Das logging und das exportieren/importieren der Spieldaten läuft aktuell auch standardmäßig über ihn.
Wichtig bei der ganzen Sache war mir vor allem das ein Bereich, sollte er abschmieren, nicht die ganze Anwendung in Mitleidenschaft zieht sondern beendet und neugestartet werden kann (auch der Globale Event Manager). Das ganze ist an Erlang angelehnt, wie ich allerdings leichtgewichtige Prozesse hinkrieg muss ich noch sehen. Ziel ist ausserdem das jedes System, sofern möglich in einem einzigen (nicht unbedingt eigenen) Thread läuft damit ich nur Zugriffe von außen Threadsicher machen muss (entweder mach ich das per async oder ich finde eine performantere Lösung).
Da soll es zumindest drauf hinaus laufen, noch ist einiges an Arbeit zu tun.
Warum es jetzt schlechter sein soll als eine globale variable ist mir allerdings schleierhaft. Ich weis das viele Leute keine Singletons mögen, allerdings hab ich bisher kaum einen anderen Grund gehört als den das es eben praktisch globale Variablen sind.
-
Probier das aus, wenn es dir Spaß macht. Der Ansatz ist aber ziemlich weltfremd, weil du einerseits deine Komponenten so weit wie möglich voneinander trennen willst, andererseits aber eine "performante Lösung" suchst. Da besteht schon einmal ein Widerspruch. In dem Durcheinander aus Diensten sind globale Variablen dann auch das geringste Problem.
Mich würden dazu tatsächlich C++-Codebeispiele interessieren. Ich kann mir gerade nämlich wenig unter Event-Managern vorstellen.
-
TyRoXx schrieb:
Der Ansatz ist aber ziemlich weltfremd, weil du einerseits deine Komponenten so weit wie möglich voneinander trennen willst, andererseits aber eine "performante Lösung" suchst. Da besteht schon einmal ein Widerspruch.
Das ist nur ein Widerspruch wenn ich versuche es syncron zu machen.
TyRoXx schrieb:
Mich würden dazu tatsächlich C++-Codebeispiele interessieren. Ich kann mir gerade nämlich wenig unter Event-Managern vorstellen.
class Spinlock { std::atomic_flag flag; public: Spinlock() : flag(ATOMIC_FLAG_INIT) { } inline void lock() { while (flag.test_and_set()); } inline void unlock() { flag.clear(); } inline bool test_and_set(){ return flag.test_and_set(); } };class EventPriorityArray { Spinlock spin; std::atomic_flag running; std::list<AbstractEvent *> queue; std::list<AbstractEvent *> queueNext; private: void performEvent(AbstractEvent *event) { event->run(); } public: void add(AbstractEvent *event) { spin.lock(); queueNext->at(event->priority()).push_back(event); spin.unlock(); } void loop() { if (!running.test_and_set()) { while (!queue.empty()) { try { AbstractEvent *event = queue.front(); if (event) performEvent(event); queue.pop_front(); } catch (std::exception e) { std::cerr << e.what() << std::endl; } } spin.lock(); std::swap(queue, queueNext); spin.unlock(); running.clear(); } } };Das ist in gekürzter Version praktisch der code den ich grade verwende. Loop verarbeit alle events und beendet sich dann, wird aber zyklisch neu gestartet (dafuer ist der global event manager zustaendig). Für das einfügen und abarbeiten werden 2 verschiedene listen verwendet, die nachdem ausführen von loop getauscht werden. Da loop nur einmal gestartet werden kann (running.test_and_set() verhindet dies) ist die Abarbeitung thread-safe. Auf die Eventklasse hab ich jetzt mal verzichtet, im Prinzip enthalten diese die Funktion die angewendet werden soll und deren Parameter (atm benutze ich std::bind fuer das meiste).
Der Spinlock ist sicher nicht die feine Art, allerdings erspart sie teure Mutexe und den relativ lahmen wechsel in den wait zustand. Da die Funktionen die locken nur eine operation durchführen sollte das dennoch im Schnitt schneller sein. Möglicherweise ersetz ich den spinlock aber auch durch eine andere Lösung. Theoretisch müsste es sogar ohne gehen, da soweit ich weiß die STL Lists Threadsafe sind. Allerdings werd ich die vermutlich durch eine nicht Thread sichere variante ersetzen, da ich sie denke ich nicht brauche.
Insgesammt mag es deutlich länger dauern bis eine einzele Operation abgeschossen wurde, das relativiert sich aber wieder. So muss der Auftraggeber nicht warten bis ein Auftrag abgeschlossen wurde (was durchaus laenger dauern kann wenn der Empfaenger beschaeftigt ist) und kann währendessen sinnvolles tun. Zudem erspare ich mir an den meisten Stellen die (teure) Synchronisation zwischen den Treads. Ganz entscheidend ist zudem die Kapselung, da nur der Eventmanager Zugriff auf die ihm unterstellten Systeme hat. Damit stelle ich sicher das alles in der richtigen Reihenfolge passiert und die Daten immer valide sind da niemand diese von aussen ändern oder lesen kann während andere Lese-/Schreibvorgänge laufen. Von der reinen Rechenzeit sollte das, gut umgesetzt (was ich mir nicht anmaßen will), vergleichbar flott wie andere (ähnlich sichere) Lösungen sein. Ein (aus meiner Sicht) entscheidender Vorteil ist das ich die einzelnen Events loops an und abschalten kann wie ich will, so kann ich sie, wenn die Rechenleistung z.b. fuer die Aufbereitung der Grafikdaten gebraucht wird, einfach pausieren.
-
seelenquell schrieb:
volkard schrieb:
Evtl ein Bus zwischen Micro-Services? Könnte mir den schon global vorstellen. Ok, die Micro-Services nicht gleich auf die harte Tour, daß jede Schnittstelle serialisierbar ist und jeder Service in jedem Land laufen kann, aber vielleicht trotz monolithischer Anwendung innendrin über den
GlobalEventManagerdie Entwickleteams mächtig entkoppelt. Diese Ebene der Entkopplung geistert mir seit zwei Wochen durch den Kopf für das verworrende Riesenprogramm, das mein Arbeitgeber seit 35 Jahren nicht aufgeräumt hat; wohl fühle ich mich dabei noch nicht.Fast richtig

Naja, eigentlich hat mich nur nen Freund gefragt ob ich (just for fun) eine Spieleengine mit ihm schreiben will. Allerdings ist dein Tip find ich garnicht soweit weg.Ich hab mehrere Sub-Systeme (z.B. ein ECS, ein Positioning System, Fight Sytem etc.) die alle auch als unabhängige libs nutzbar sein sollen. Genauer gesagt ist sogar alles so ausgelegt das die Systeme selbst überhaupt nichts eigenständig tun (können), alles läuft asynchron über einen Event Manager. Theoretisch spielts damit auch keine Rolle ob die Systeme auf dem selben PC laufen oder am anderen Ende der Welt. Genau das ist auch das Ziel, alles soweit voneinander zu trennen das genau das möglich wäre.
Ich hab das ganze ausserdem in Realms organisiert, jeder Realm stellt quasi eine eigene Welt dar mit jeweils eigenen Systemen. Ob diese fuer den Realm erstellt werden, oder sich mehrere Realms ein System teilen kann man frei konfigurieren.
Der GlobalEventManager ist im Prinzip auch wieder nur ein normaler Event Manager, nur das er eben die Hoheit über alles hat. So läuft z.b. die komunikation zwischen den Realms standardmäßig über den GEM, er kann andere Eventmanager beenden, Realms zerstören und erschaffen. Das logging und das exportieren/importieren der Spieldaten läuft aktuell auch standardmäßig über ihn.
Wichtig bei der ganzen Sache war mir vor allem das ein Bereich, sollte er abschmieren, nicht die ganze Anwendung in Mitleidenschaft zieht sondern beendet und neugestartet werden kann (auch der Globale Event Manager). Das ganze ist an Erlang angelehnt, wie ich allerdings leichtgewichtige Prozesse hinkrieg muss ich noch sehen. Ziel ist ausserdem das jedes System, sofern möglich in einem einzigen (nicht unbedingt eigenen) Thread läuft damit ich nur Zugriffe von außen Threadsicher machen muss (entweder mach ich das per async oder ich finde eine performantere Lösung).
Da soll es zumindest drauf hinaus laufen, noch ist einiges an Arbeit zu tun.
Faszinierend.

Ich mache zwar was total anderes, gefühlte mehr als 180 Grad entgegengesetzt (1), aber alle Deine Fragen sind auch meine. Bis aufs Abschmieren, das plötzlich abzustellen würden unsere Kunden nicht ertragen.(1) /me denkt dabei irgendwie erfreut an klassenmethode, der den angenommenen Zahlenbereich ins Subversive erweitert.
-
volkard schrieb:
Faszinierend.

Ich mache zwar was total anderes, gefühlte mehr als 180 Grad entgegengesetzt (1), aber alle Deine Fragen sind auch meine. Bis aufs Abschmieren, das plötzlich abzustellen würden unsere Kunden nicht ertragen.(1) /me denkt dabei irgendwie erfreut an klassenmethode, der den angenommenen Zahlenbereich ins Subversive erweitert.
Naja, wie ich das mit dem Abschmieren letztenendes handle steht auf nem ganz anderen Blatt, ich glaub das wird nen richtig hartes Stück Arbeit

-
void loop() { if (!running.test_and_set()) { while (!queue.empty()) { try { AbstractEvent *event = queue.front(); if (event) performEvent(event); queue.pop_front(); } catch (std::exception e) { std::cerr << e.what() << std::endl; } } } spin.lock(); std::swap(queue, queueNext); spin.unlock(); running.clear(); }Das kann so nicht funktionieren.
Ich vermute was du machen wolltest ist:lock if running unlock, exit running = true swap queue, queueNext unlock for each (even in queueNext) process event clear queueNext lock running = false unlockUnd was Spinlocks angeht: üblicherweise sind lokale Mutexen als Spin-Sleep- bzw. Spin-Wait-Locks implementiert. Zumindest unter Windows kann ich es dir garantieren (ganz sicher
CRITICAL_SECTION, zu 99.9% auch die SRW-Locks). BeiCRITICAL_SECTIONkannst du dir sogar den Spin-Count aussuchen. Der Overhead verglichen mit selbstgebastelten Spinlocks ist relativ gering. Der klare Vorteil ist dass du, wenn ein besitzender Thread mal heia geht, viel weniger Rechenzeit mit busy-waiting verbrätst. Ein weiterer Vorteil ist dass die Spin-Locks vom OS normalerweise so implementiert sind dass sie halbwegs "resourcenschonend" spinnen. D.h. auf x86 eben gerade nicht mit einem "atomic test and set" Befehl, sondern erstmal mit stinknormalen "relaxed" Loads so lange warten bis überhaupt ne Chance besteht dass der folgende "atomic test and set" Erfolg hat.In ganz speziellen Fällen mag es trotzdem Sinn machen selbst 'was zu basteln, aber i.A. würde ich dir auf Windows SRW-Locks ans Herz legen. Bzw. wenns plattformübergreifend sein soll einfach mal gucken wie
std::mutexsich im Vergleich macht, bevor du beschliesst dass da einfach ein untragbarer Overhead sein muss.Bzw. ansonsten guck dich nach einer threadsicheren List-implementierung um. Vielleicht
boost::lockfree::queue. (Dassstd::listthread safe sein soll ist mir neu. Bzw. ich bin mir sehr sicher dass es nicht so ist.)
-
hustbaer schrieb:
Das kann so nicht funktionieren.
Hoppla, da ist wohl die klammer des ifs verrutscht. So sollte es richtig sein:
void loop() { if (!running.test_and_set()) { while (!queue.empty()) { try { AbstractEvent *event = queue.front(); if (event) performEvent(event); queue.pop_front(); } catch (std::exception e) { std::cerr << e.what() << std::endl; } } spin.lock(); std::swap(queue, queueNext); spin.unlock(); running.clear(); } }hustbaer schrieb:
Ich vermute was du machen wolltest ist:
lock [...] lock running = false unlockIch wollte es schlicht lockfrei machen, der ist auch garnicht nötig, da im Falle running=true die funktion wieder verlassen wird und atomic_flag lockfree ist.
Folgendes wäre vermutlich deutlicher gewesen:if (running.test_and_set()) { return; }hustbaer schrieb:
In ganz speziellen Fällen mag es trotzdem Sinn machen selbst 'was zu basteln, aber i.A. würde ich dir auf Windows SRW-Locks ans Herz legen. Bzw. wenns plattformübergreifend sein soll einfach mal gucken wie
std::mutexsich im Vergleich macht, bevor du beschliesst dass da einfach ein untragbarer Overhead sein muss.Ich bin mir den Kosten eines spinlocks durchaus bewusst.
Mutexe haben in meinem Test recht schlecht abgeschnitten. Simple dargestellt summiert es sich eben auf wenn man mehrere 1000 male Lockt (mit den damit verbunden kosten) statt jeweils nur 3 Pointer zu setzen.hustbaer schrieb:
Bzw. ansonsten guck dich nach einer threadsicheren List-implementierung um. Vielleicht
boost::lockfree::queue. (Dassstd::listthread safe sein soll ist mir neu. Bzw. ich bin mir sehr sicher dass es nicht so ist.)Jo, stimmt, da hatte ich mich wohl verlesen. Aber ich brauche eben grade keine Thread sichere Liste wie ich schon sagte. Inzwischen ist auch auf eine eigene Liste umgestellt (aber noch nicht getestet), das Element wird vorher konstruiert, das event eingefügt und erst nach dem spinlock werden die 3 pointer (eigentlich kommt ja oben noch die Konstruktion und der Pointer fuer das Event obendrauf, aber scheinbar ist auch das schneller) gesetzt.
Nicht das du jetzt denkst ich nehme deine Tips nicht ernst, ich wollte nur sagen das ich eben auch meine Gründe habe es so zu machen. Generell bin ich für jeden Tip dankbar (ausgenommen jene die darauf hinaus laufen meine frage nicht zu Beantworten) und gegebenefalls werde ich mir die Windows Threading geschichten anschauen.
-
Ich sehe gerade du fügst in die Liste
queueNextein, nicht inqueuewie ich angenommen hatte. (Vermutlich, da deinaddCode nicht zum Rest passt, da der wohl für nenvector<list<...geschrieben wurde -- und das "Priority" im Namen der Klasse wäre sonst auch etwas unpassend.)Ja, in dem Fall, und wenn du den Lock hältst während du in
queueNexteinfügst, dann sollte das vermutlich funktionieren. Allerdings frage ich mich wieso du erst abarbeitest und dann tauscht - dadurch bist du doch immer einenloop-Aufruf hinten?Und... guck dir den Code nochmal an. Was du da geschrieben hast ist nichts anderes als
Spinlock nextQueueMutex; Spinlock loopMutex; void loop() { if (loopMutex.try_lock()) { while (!queue.empty()) { try { AbstractEvent *event = queue.front(); if (event) performEvent(event); queue.pop_front(); } catch (std::exception e) { std::cerr << e.what() << std::endl; } } nextQueueMutex.lock(); std::swap(queue, queueNext); nextQueueMutex.unlock(); loopMutex.unlock(); } }Damit "siehst" du was du hier hast, nämlich nichts anderes als eine "Try-Mutex" (dass diese als Spinlock implementiert ist spielt ja keine Rolle wenn es um das Konzept "Mutex" geht). Und jetzt sieht man auch schön dass hier nichts wirklich lock-free ist.
Als nächstes... weder deine
loop-Funktion noch deineadd-Funktion sind Exception-safe.
Beiloopgibt es zwei Probleme:- Wenn etwas anderes als
std::exceptionfliegt bist du tot. - Wenn
performEventbei einem bestimmten Event immer eine Exception wirft bist du ebenso tot. Dann versucht die Schleife nämlich immer wieder und wieder den selben Event zu bearbeiten, was immer wieder und wieder eine Exception wirft, wodurch der Event dann nie aus der Queue genommen wird. (Was dasif (event)da macht ist mir auch nicht ganz klar - null Pointer muss man doch gar nicht erst einfügen?)
Und was
addangeht...
std::listbesorgt inpush_backdynamisch Speicher. Das kann schief gehen. Der Fall ist zugegebenermassen selten und ein (nicht-triviales) Programm das an allen möglichen Stellen mitbad_allocklarkommt ist verflixt schwer bis unmöglich zu schreiben... aber wenn es einem schon so ins Gesicht springt wie hier, dann sollte man den Fall behandeln. Einfachste Lösung:noexceptdranschreiben, dann bricht der Compiler im Falle des Falles das Programm einfach ab.Bessere Lösung:
std::unique_lock(für dastry_lockinloop) bzw.std::lock_guard(für den Rest) verwenden.Aber nochmal zurück zu "
std::listbesorgt inpush_backdynamisch Speicher". Das heisst automatisch auch dass du den Lock für wesentlich länger als "drei Zeiger vertauschen" hältst, nämlich für so lange wie halt der Allocator braucht um ne Neue Node zu besorgen. Es heisst auch dass du Performance verschenkst. Lösung: verwende eine "intrusive" Datenstruktur (siehe z.B. Boost.Intrusive). So kannst du den Speicher besorgen bevor du die "list mutex" lockst, das Einfügen beschränkt sich dann wirklich auch ein paar einfache Operationen mit Zeigern.Und nochmal zum Thema Performance: wenn es dir um Performance geht, dann solltest du nicht die Default-Argumente von
test_and_setbzw.clearverwenden, denn mit denen bekommst du unnötig schlechte Performance. Da es hier um ne Try-Mutex geht sollte fürlock/try_lockdie orderstd::memory_order_acquireund fürunlockdie orderstd::memory_order_releaseausreichend sein. Mehr garantieren schliesslich die Standard-Mutexen auch nicht (und auch die meisten Thirdparty Mutexen nicht). Zumindest auf x86 macht das nen Unterschied, denn bei x86 istclear(std::memory_order_release)ein einfachesmov,clear(mit Default-Argumentstd::memory_order_seq_cst) ist dagegen ein wesentlich teureresxchg.
(Es mag sein dass manche Compiler zu doof sindclear(std::memory_order_release)zu einem einfachenmovzu optimieren, aber grundsätzlich ist es möglich, und zumindest aktuelle GCC Versionen sind schlau genug.)ps
Mutexe haben in meinem Test recht schlecht abgeschnitten.
Auf welcher Plattform (OS, Compiler) und mit welcher Mutex-Klasse hast du getestet? Und was bedeutet "recht schlecht"?
- Wenn etwas anderes als
-
seelenquell schrieb:
Die frage ist jetzt ob ich beim Meyers Singleton die statische Variable auch als Pointer deklarieren kann (sprich den speicher auf dem Heap reserviern kann)
...Angenommen, Deine großen collections sind keine array in der Klasse, sondern ganz normale C++ container, dann alloziieren die Ihren Speicher immer vom heap, auch wenn sie selbst statisch alloziiert sind.
Ansonsten wäre es aber auch thread save.
Und Warnung: Singletons sind in der Regel ein Designfehler.
-
hustbaer schrieb:
Ich sehe gerade du fügst in die Liste
queueNextein, nicht inqueuewie ich angenommen hatte. (Vermutlich, da deinaddCode nicht zum Rest passt, da der wohl für nenvector<list<...geschrieben wurde -- und das "Priority" im Namen der Klasse wäre sonst auch etwas unpassend.)Jup, da hast du recht, eigentlich wars ein std::array<List ..., 16>. Die 2 listen sind einfach dafür da, das sie beim einfügen und löschen nicht aufeinander laufen.
hustbaer schrieb:
Ja, in dem Fall, und wenn du den Lock hältst während du in
queueNexteinfügst, dann sollte das vermutlich funktionieren. Allerdings frage ich mich wieso du erst abarbeitest und dann tauscht - dadurch bist du doch immer einenloop-Aufruf hinten?Da ich überlegt hatte eine externe Unterbrechung der Schleife vorzusehen (z.B. vom Global Event Manager) falls ich die Rechenleistung kurzfristig anderweitig brauche (z.B. für Grafikberechnungen). Würde ich jetzt die Schleife verlassen wenn noch nicht alle events abgearbeitet sind, würde beim nächsten Aufruf getauscht und nicht die events die zuerst anstehen abgearbeitet, sondern die aus dem anderen Container die eigentlich erst hinterher dran sind. Ich fand das so am übersichtlichsten, dann brauche ich nicht am Anfang noch nen if vor das tauschen reinwurschsteln (vermutlich brauch ichs eh, aber das kommt noch).
hustbaer schrieb:
Damit "siehst" du was du hier hast, nämlich nichts anderes als eine "Try-Mutex" (dass diese als Spinlock implementiert ist spielt ja keine Rolle wenn es um das Konzept "Mutex" geht). Und jetzt sieht man auch schön dass hier nichts wirklich lock-free ist.
Technisch gesehen ist vermutlich nichts wirklich lockfrei. Aber versteh bitte das ich keine "richtigen" Mutexe/Locks einsetzen will, das will ich unter allen Umstaenden vermeiden. Mir gehts dabei ums Prinzip, ich will darauf hinaus am Ende keine "sichtbaren" Locks zu haben. Ich hatte vor Jahren mal zimliche schwierigkeiten mit Locks, vermutlich habe ich desswegen so lange Clojure programmiert, wo es sprachlich garkeine Möglichkeit gibt Locks zu setzen, das war eine wirkliche Offenbarung. Daher kenne ich auch die Konzepte von Atomic, STM und async recht gut, in C++ muss ich mich da allerdings nochmal richtig durchwühlen (wie bei vielen anderen dingen auch).
hustbaer schrieb:
Als nächstes... weder deine
loop-Funktion noch deineadd-Funktion sind Exception-safe.
Beiloopgibt es zwei Probleme:- Wenn etwas anderes als
std::exceptionfliegt bist du tot. - Wenn
performEventbei einem bestimmten Event immer eine Exception wirft bist du ebenso tot. Dann versucht die Schleife nämlich immer wieder und wieder den selben Event zu bearbeiten, was immer wieder und wieder eine Exception wirft, wodurch der Event dann nie aus der Queue genommen wird.
Das ist ein guter Punkt, in exceptions muss ich mich noch richtig einarbeiten, da hab ich noch zimliche defizite. Das mit der Schleife stimmt natuerlich und wird angepasst.
hustbaer schrieb:
(Was das
if (event)da macht ist mir auch nicht ganz klar - null Pointer muss man doch gar nicht erst einfügen?)Jo, das ist in dem Beispiel natürlich unsinnig und ein Überbleibsel aus dem ursprünglichen Code. Da wird in der Liste nur ein Verweis auf das Event gespeichert, welches in einer anderen Collection gespeichert ist. Bei Realmübergreifenden events kanns sein das dieses schon abgearbeitet ist und die Collection null zurueck gibt. Das das nicht Performant ist, ist klar, allerdings habe ich da schon die ein oder andere Idee.
hustbaer schrieb:
Aber nochmal zurück zu "
std::listbesorgt inpush_backdynamisch Speicher". Das heisst automatisch auch dass du den Lock für wesentlich länger als "drei Zeiger vertauschen" hältst, nämlich für so lange wie halt der Allocator braucht um ne Neue Node zu besorgen. Es heisst auch dass du Performance verschenkst. Lösung: verwende eine "intrusive" Datenstruktur (siehe z.B. Boost.Intrusive). So kannst du den Speicher besorgen bevor du die "list mutex" lockst, das Einfügen beschränkt sich dann wirklich auch ein paar einfache Operationen mit Zeigern.Jup, hatte ich ja schon geschrieben. Meine List Implementierung hab ich mal auf pastebin gestellt: http://pastebin.com/ZM6Q2eig
Ist allerdings alles andere als fertig und ich kanns grad nicht testen. Vermutlich werd ich die dann auch in einem Atomic speichern (afaik erfüllt sie die Vorrausetungen dafür).Boost finde ich zwar Klasse, dennoch möchte ich für triviale fälle vermeiden da Boost einfach Riesig ist und meine lib nicht davon abhängig sein soll (ich verwende aktuell zwar property_tree, das lässt sich aber per #ifndef SECS_NOBOOST abschalten).
hustbaer schrieb:
Und nochmal zum Thema Performance: wenn es dir um Performance geht, dann solltest du nicht die Default-Argumente von
test_and_setbzw.clearverwenden, denn mit denen bekommst du unnötig schlechte Performance. Da es hier um ne Try-Mutex geht sollte fürlock/try_lockdie orderstd::memory_order_acquireund fürunlockdie orderstd::memory_order_releaseausreichend sein. Mehr garantieren schliesslich die Standard-Mutexen auch nicht (und auch die meisten Thirdparty Mutexen nicht). Zumindest auf x86 macht das nen Unterschied, denn bei x86 istclear(std::memory_order_release)ein einfachesmov,clear(mit Default-Argumentstd::memory_order_seq_cst) ist dagegen ein wesentlich teureresxchg.
(Es mag sein dass manche Compiler zu doof sindclear(std::memory_order_release)zu einem einfachenmovzu optimieren, aber grundsätzlich ist es möglich, und zumindest aktuelle GCC Versionen sind schlau genug.)Sehr intressant, danke. Die semantiken kenne ich zwar, aber einen genaueren einblick hatte ich da natürlich nicht. Zu dem Zeitpunkt als ichs geschrieben hatte sollte es zunächst einfach nur funktionieren und möglichst simpel sein (ich habe damit ne andere Collection ersetzt). Anpacken muss ichs sowieso nochmal, da werd ich das beherzigen.
hustbaer schrieb:
ps
Mutexe haben in meinem Test recht schlecht abgeschnitten.
Auf welcher Plattform (OS, Compiler) und mit welcher Mutex-Klasse hast du getestet? Und was bedeutet "recht schlecht"?
[/quote]
Schon eine weile her, aber ich meine es wäre um den Faktor 4 rum gewesen. Getestet habe ich auf Windows mit dem 64bit mingw-builds-win-sjlj (mit der Implementation des Singletons habe ich dann auf mingw-builds-posix-sjlj umgestellt da die win variante mit std::call_once etc nicht klarkommt). Zum habe ich glaube ich std::lock_guard verwendet.
Letzten Endes ists aber egal, da ich mich schon dafür entschlossen habe keine Mutexe zu verwenden und Locks konsequent zu vermeiden/rauszuschmeissen sofern irgenwie möglich. In der Beziehung lass ich auch nicht mit mir reden.Torsten Robitzki schrieb:
Angenommen, Deine großen collections sind keine array in der Klasse, sondern ganz normale C++ container, dann alloziieren die Ihren Speicher immer vom heap, auch wenn sie selbst statisch alloziiert sind.
Ansonsten wäre es aber auch thread save.
Danke für die Antwort, irgendwer meinte zwar etwas Ähnliches, da war ich mir aber nicht sicher ob er auch explizit Pointer meinte.
Torsten Robitzki schrieb:
Und Warnung: Singletons sind in der Regel ein Designfehler.
Ehrlichgesagt kann ich das mittlerweile nicht mehr glauben. Ich habe das Argument schon unendlich oft gehört aber nie eine richtige Begründung der Aussage. Klar sind sie im Prinzip globale Variablen, im Gegensatz dazu aber gekampselt, was insgesammt auch nur ein Problem ist wenn ich von überall im Code darauf referenziere. Tue ich das nicht, sondern übergebe ich sie an den stellen in denen ich sie brauche habe ich kein Problem damit. Dagegen ist es viel Schwerwiegender wenn eine Resource die nur ein einzelnes mal vorkommen darf mehrmals vorkommt (oder kann).
- Wenn etwas anderes als
-
seelenquell schrieb:
Ehrlichgesagt kann ich das mittlerweile nicht mehr glauben. Ich habe das Argument schon unendlich oft gehört aber nie eine richtige Begründung der Aussage. Klar sind sie im Prinzip globale Variablen, im Gegensatz dazu aber gekampselt, was insgesammt auch nur ein Problem ist wenn ich von überall im Code darauf referenziere. Tue ich das nicht, sondern übergebe ich sie an den stellen in denen ich sie brauche habe ich kein Problem damit. Dagegen ist es viel Schwerwiegender wenn eine Resource die nur ein einzelnes mal vorkommen darf mehrmals vorkommt (oder kann).
Es gibt in C++ ein banales Sprachmittel, um auszudrücken, dass eine Ressource einmal vorkommt: Parameterübergabe.
void f(resource &singleton) { //... } struct user { explicit user(resource &singleton); };Ta-da! Problem gelöst. Wenn man keine Singletons verwendet, stellt sich in
fund in denuser-Methoden gar nicht die Frage nach weiteren Instanzen vonresource.Es geht bei Singletons nicht darum, 1-zu-N-Beziehungen zu modellieren oder irgendwie die Korrektheit zu fördern. Hört doch auf das zu behaupten. Es geht um erstens Faulheit und zweitens Unfähigkeit saubere Beziehungen zwischen Komponenten zu finden. Man muss sich überwinden das mal vernünftig zu lernen. Irgendwann macht es dann *klick* und man hat keine Angst mehr vor Klassen und Referenzen.
Ich bin sehr an Beispielen für sinnvolle Verwendung von Singletons interessiert. Habe noch nie eins gesehen.Wenn ich deinen Code lese, sehe ich da ungefähr einen
io_service:class EventPriorityArray { boost::asio::io_service m_queue; public: void add(AbstractEvent *event) { m_queue.post([event] { event->run(); }); } void loop() { m_queue.poll(); } };Der hat nur eine Queue und nicht zwei wie du. Dein Ansatz mit
loopist nämlich seltsam. Warum machst du das so? Mit fehlt jetzt noch das ganze Bild. Wer ruftloopauf?Verkettete Listen sind für Performance übrigens das schlechteste, was man nehmen kann.
-
Nabend :-),
seelenquell schrieb:
Ehrlichgesagt kann ich das mittlerweile nicht mehr glauben. Ich habe das Argument schon unendlich oft gehört aber nie eine richtige Begründung der Aussage.
Ich habe das in dem Satz ja nur postuliert, ein Argument hast Du von mir noch überhaupt nicht gehört

seelenquell schrieb:
Klar sind sie im Prinzip globale Variablen, im Gegensatz dazu aber gekampselt, was insgesammt auch nur ein Problem ist wenn ich von überall im Code darauf referenziere.
Ich habe überhaupt nichts gegen globale Variablen, solange sie ein guter Weg sind, das gestellte Problem zu lösen. std::cout, std::clog, std::cerr sind übrigens alles globale Variablen.
Mein Problem mit singletons ist, dass sie überhaupt kein Problem lösen. Das ursprüngliche Ziel war, sicher zu stellen, dass es exakt nur eine Instanz einer Klasse in einem Programm gibt. Aber warum sollte ich das wollen? Welche Anforderung kann ich damit erfüllen bzw. welches Design Ziel erreichen?
Kapselung, wie Du es schreibst, ist kein Design-Ziel. Lose Koppelung kann man durch Kapselung erreichen, da dadurch die Schnittstelle zu einer Komponente schmaler wird. Singletons führen aber zu allem anderen, aber nicht zu lose gekoppelten Komponenten. Du kannst ja mal versuchen, aus einem System mit vielen Singletons irgend etwas wieder zu verwenden. Meistens ziehst Du da an einem Ende und über die Singletons, hast Du dann das ganze System in der Hand.
In C++ setzt man singletons meist gar nicht für das ursprünglich Ziel ein, sondern um Probleme mit Initialisierungsreihenfolgen zu lösen. Dagegen spricht auch nichts, das sind dann Implementierungsdetails und so etwas möchte ich nicht als Schnittstelle bedienen müssen.
log::instance()->log_debug( "Foo" ); // vs. log::log_debug( "Foo" );da interessiert es mich doch nicht, dass die logging Komponente intern als Singleton implementiert ist, damit ich zu jedem Zeitpunkt darauf zugreifen kann.
mfg Torsten
P.S.: Anekdoten:
https://groups.google.com/d/topic/de.comp.objekt/i4Ana3y_tGc/discussion
https://www.xing.com/communities/posts/singletons-pro-strich-con-1004722838
-
TyRoXx schrieb:
Es gibt in C++ ein banales Sprachmittel, um auszudrücken, dass eine Ressource einmal vorkommt: Parameterübergabe.
[...]
Ta-da! Problem gelöst. Wenn man keine Singletons verwendet, stellt sich infund in denuser-Methoden gar nicht die Frage nach weiteren Instanzen vonresource.Ich stimme dir da voll zu das die Übergabe als Parameter wohl am Sinnvollsten ist, wie ich schonmal geschrieben hatte, und was auch Torsten Robitzki (siehe Unten) angesprochen hat gings mir dabei hauptsächlich initialisierungsprobleme zu lösen. Ich werde das Singleton für den Global Event Manager vermutlich auch als hauptsächlich als interne statische Variable (oder Klasse) der normalen Eventmanager Klasse verwenden, damit jeder Eventmanager bestimmte Informationen an den Globalen Eventmanager weiterleiten kann (z.b. registrieren sich diese dort). Der Global Event Manager erbt auch nicht von der Eventmanager KLasse, er steuert diese aber. Wie das am Ende genau aussieht wird sich noch zeigen.
TyRoXx schrieb:
Es geht bei Singletons nicht darum, 1-zu-N-Beziehungen zu modellieren oder irgendwie die Korrektheit zu fördern. Hört doch auf das zu behaupten. Es geht um erstens Faulheit und zweitens Unfähigkeit saubere Beziehungen zwischen Komponenten zu finden. Man muss sich überwinden das mal vernünftig zu lernen. Irgendwann macht es dann *klick* und man hat keine Angst mehr vor Klassen und Referenzen.
Ich bin sehr an Beispielen für sinnvolle Verwendung von Singletons interessiert. Habe noch nie eins gesehen.Naja, was die Initialisierung angeht finde ich schon das es die Korrektheit fördern kann, zumindest erleichters es erheblich zur rechten Zeit das voll initialisierte Objekt zur Hand zu haben, sicher gibts da andere Wege aber ich finde Singletons richtig eingesetzt immer noch nicht übel. Das es was mit Faulheit zu tun hat den einfacheren Ansatz zu wählen, da ist sicher was wahres dran. Ein einfaches Design kann aber auch Vorteile haben, unoetige Komplexität ist häufig eine Fehlerquelle. Ob es was mit Unfähigkeit zu tun hat wird sich in meinem Fall noch zeigen. Falls du einen Vorschlag hast wie ich meine 1:N Beziehungen, inclusive ordentlicher Initialisierung möglichst ausserhalb der Main hinbekommen werde ich mir den gerne anschauen.
TyRoXx schrieb:
Wenn ich deinen Code lese, sehe ich da ungefähr einen
io_service:
[...]
Der hat nur eine Queue und nicht zwei wie du. Dein Ansatz mitloopist nämlich seltsam. Warum machst du das so? Mit fehlt jetzt noch das ganze Bild. Wer ruftloopauf?boost::asio werd ich mir mal anschauen.
Die 2 Querys habe ich desswegen damit in die eine ungestört eingefügt, und aus der anderen rausgeholt werden kann. So beinflussen sich die beiden Funktionen möglichst wenig. Sonst müsste ich entweder die Liste jedes mal komplett sperren oder ich muss aufwendig checken das nicht beide Funktionen auf das gleiche Element zugreifen. Sicher gibts noch Threadsichere alternativen, aber da es speicher- und performancetechnisch relativ egal ist ob ich eine oder 2 Listen verwende habe ich mich für diese einfache Alternative entschieden.Loop wird im übrigen in der loop in der main funktion (meiner TestApp) aufgerufen, später soll das der Global Event Manager erledigen, dessen loop dann in der main zyklisch neu gestartet wird (ich meine mal gelesen zu haben das die Hauptschleife möglichst immer in der main sein sollte).
TyRoXx schrieb:
Verkettete Listen sind für Performance übrigens das schlechteste, was man nehmen kann.
Ja das stimmt leider, ich bin noch am überlegen was ich am besten mache. Sicherlich wäre eine Array-Queue (ich hab die auch schon als Listen oder Trees gesehen) in der richtigen Größe das beste. Ich hoffe insgeheim aber noch das ich der Zeit in der auf die Daten vom Heap geholt werden (beim Zugriff auf das listenelement) was anderes tun kann, Erlang ist bei sowas extrem gut das es Prozesse extrem schnell switchen kann.
Torsten Robitzki schrieb:
Ich habe überhaupt nichts gegen globale Variablen, solange sie ein guter Weg sind, das gestellte Problem zu lösen. std::cout, std::clog, std::cerr sind übrigens alles globale Variablen.
Mein Problem mit singletons ist, dass sie überhaupt kein Problem lösen. Das ursprüngliche Ziel war, sicher zu stellen, dass es exakt nur eine Instanz einer Klasse in einem Programm gibt. Aber warum sollte ich das wollen? Welche Anforderung kann ich damit erfüllen bzw. welches Design Ziel erreichen?
Kapselung, wie Du es schreibst, ist kein Design-Ziel. Lose Koppelung kann man durch Kapselung erreichen, da dadurch die Schnittstelle zu einer Komponente schmaler wird. Singletons führen aber zu allem anderen, aber nicht zu lose gekoppelten Komponenten. Du kannst ja mal versuchen, aus einem System mit vielen Singletons irgend etwas wieder zu verwenden. Meistens ziehst Du da an einem Ende und über die Singletons, hast Du dann das ganze System in der Hand.
In C++ setzt man singletons meist gar nicht für das ursprünglich Ziel ein, sondern um Probleme mit Initialisierungsreihenfolgen zu lösen. Dagegen spricht auch nichts, das sind dann Implementierungsdetails und so etwas möchte ich nicht als Schnittstelle bedienen müssen.
Genau dafuer ist es auch gedacht. Das mit den lose gekoppelten Komponenten ist ein guter Punkt. Mehr oder weniger meinte ich das mit "überall referenzieren", problematisch ist das aber auch mit Globalen variablen (cout aus nem Projekt zu schmeissen das auf ausgabe spezialisiert ist, ist bestimmt auch interessant). Das es nur eine Instanz geben soll liegt eben daran Begründet das der Global Event Manager alles andere verwalten soll, ob es letztenende notwendig ist das es nur eine Instanz geben kann wird sich zeigen, momentan ists die einfachste Lösung.
Kapselung ist da sicher nicht ganz der richtige, aber verwende Begriffe wie Kapselung, Schnittstelle und lose Kopplung oft synonym, was sicher nicht ganz richtig ist aber ich finde mehr oder weniger laeufts auf das Gleiche hinaus (Lose Kopplung ereiche ich über ne gute Schnittstellendefinition und das sich die Leute dran halten über Kapselung). Ich bin auch nicht ganz so fit was "normale" Softwareentwicklung angeht, ist auch einer der Gründe warum ich das meiste selbst machen will, aus Fehlern lernt man schliesslich.
Torsten Robitzki schrieb:
log::instance()->log_debug( "Foo" ); // vs. log::log_debug( "Foo" );da interessiert es mich doch nicht, dass die logging Komponente intern als Singleton implementiert ist, damit ich zu jedem Zeitpunkt darauf zugreifen kann.
Jup, ob ich überhaupt ne getInstance methode brauche werde ich sehen, momentan siehts eher nicht danach aus, aber ursprünglich hat der GEM von normalen Eventmanager geerbt, da war es natürlich einfacher die Methoden über instance aufzurufen als für alles eine statische Methode zu schreiben und weiterzuleiten. Mitlerweise ist er als statische Variable in die eventmanager Klasse hinein gewandert, da Größtenteils nur diese ihn verwenden, möglicherweise wandert er als private Klass auch ganz rein.
Torsten Robitzki schrieb:
P.S.: Anekdoten:
https://groups.google.com/d/topic/de.comp.objekt/i4Ana3y_tGc/discussion
https://www.xing.com/communities/posts/singletons-pro-strich-con-1004722838Ich werd mal reinschauen

Dank eurer Argumente für das Für und Wider von Singletons bin ich jetzt schonmal schlauer, bestärkt mich aber auch darin das Singletons in meinem Fall nicht unbedingt verkehrt sind, ob ich sie langfristig noch brauche wird sich zeigen.
Ich hoffe ich hab nicht zu viel Mist geschrieben, ist schon spät

Grüße
-
seelenquell schrieb:
Ich werde das Singleton für den Global Event Manager vermutlich auch als hauptsächlich als interne statische Variable (oder Klasse) der normalen Eventmanager Klasse verwenden, damit jeder Eventmanager bestimmte Informationen an den Globalen Eventmanager weiterleiten kann (z.b. registrieren sich diese dort).
Sowas haben wir sicherlich auch öfter mal in unserer Software... Nur stört es immer, wenn man dann plötzlich mehreren Instanzen von dem ganzen System haben will. Das kann man im Voraus alles schlecht abschätzen. Irgendwann wird dein System (nochmal) viel größer, und dann gibts lauter neue Entwickler und Anforderungen, und jemand sagt, ja, dieses Event Zeugs ist cool, will ich auch verwenden, aber mein Sub System hat nichts mit dem Rest zu tun und der globale Event Manager soll davon gar nichts mitbekommen.
-
seelenquell schrieb:
Technisch gesehen ist vermutlich nichts wirklich lockfrei.
Doch, lock-freie Datenstrukturen. Dinge wie try_lock bei einer Try-Mutex sind auch lock-free.
seelenquell schrieb:
Aber versteh bitte das ich keine "richtigen" Mutexe/Locks einsetzen will, das will ich unter allen Umstaenden vermeiden. Mir gehts dabei ums Prinzip, ich will darauf hinaus am Ende keine "sichtbaren" Locks zu haben. Ich hatte vor Jahren mal zimliche schwierigkeiten mit Locks, vermutlich habe ich desswegen so lange Clojure programmiert, wo es sprachlich garkeine Möglichkeit gibt Locks zu setzen, das war eine wirkliche Offenbarung. Daher kenne ich auch die Konzepte von Atomic, STM und async recht gut, in C++ muss ich mich da allerdings nochmal richtig durchwühlen (wie bei vielen anderen dingen auch).
Sorry, aber das klingt alles nicht so als ob du genügend Grundlagen/Vorkenntnisse hättest um sowas erfolgreich auf die Beine zu stellen.
-
Ist eine Weile her, aber ich hatte viel tun (nicht nur Programmieren).
Mechanics schrieb:
seelenquell schrieb:
Ich werde das Singleton für den Global Event Manager vermutlich auch als hauptsächlich als interne statische Variable (oder Klasse) der normalen Eventmanager Klasse verwenden, damit jeder Eventmanager bestimmte Informationen an den Globalen Eventmanager weiterleiten kann (z.b. registrieren sich diese dort).
Sowas haben wir sicherlich auch öfter mal in unserer Software... Nur stört es immer, wenn man dann plötzlich mehreren Instanzen von dem ganzen System haben will. Das kann man im Voraus alles schlecht abschätzen. Irgendwann wird dein System (nochmal) viel größer, und dann gibts lauter neue Entwickler und Anforderungen, und jemand sagt, ja, dieses Event Zeugs ist cool, will ich auch verwenden, aber mein Sub System hat nichts mit dem Rest zu tun und der globale Event Manager soll davon gar nichts mitbekommen.
Jup, könnte Aufwändig werden das im nachhinein auseinander zu wurschteln.
Das dachte ich mir letztenendes auch und hab einiges umgestellt. Den GlobalEventManager selbst ist nun kein Singleton mehr (und heisst nun EventServer), die Konstruktoren sind protected. Mein Singleton EventServer der intern verwendet wird erbt einfach von der KLasse.Das lässt sich per #define ECS3P_CUSTOM_EVENTSERVER CustomClass, auch einstellen. Der Rest ist eigentlich per Template Parameter konfigurierbar aber das ganze sah beim debuggen relativ unschön aus (bei typedef SystemManger<EventManagerBase, EventServer> EventManger; wobei der EventServer auch wieder ein Template ist wird leider immer der volle name incl aller paramter ausgegeben).
Deshalb erben EventManager und co die einzelnen Template Klassen einfach (aka class Eventmanager : public SystemManager<EventManager, EventServer_t>), EventServer_t wird dann einfach in einem #ifdef block als typedef gesetzt. Ob das so ideal ist kann ich nicht sagen, allerdings wars für die simpelste Lösung das von Benutzer konfigurierbar zu machen. Auf dem gleiche weg lassen sich auch Systeme komplett deaktivieren.
hustbaer schrieb:
seelenquell schrieb:
Technisch gesehen ist vermutlich nichts wirklich lockfrei.
Doch, lock-freie Datenstrukturen. Dinge wie try_lock bei einer Try-Mutex sind auch lock-free.
Muss ich mir mal anschauen, eigentlich waere mir STM ja am liebsten, aber bis 2017 werde ich da vermutlich noch mindestens warten müssen wenn ich keine externe lib verwenden will. Je nachdem wie lange es dauert bis die Compiler es unterstützen eher noch länger.
hustbaer schrieb:
seelenquell schrieb:
Aber versteh bitte das ich keine "richtigen" Mutexe/Locks einsetzen will, das will ich unter allen Umstaenden vermeiden. Mir gehts dabei ums Prinzip, ich will darauf hinaus am Ende keine "sichtbaren" Locks zu haben. Ich hatte vor Jahren mal zimliche schwierigkeiten mit Locks, vermutlich habe ich desswegen so lange Clojure programmiert, wo es sprachlich garkeine Möglichkeit gibt Locks zu setzen, das war eine wirkliche Offenbarung. Daher kenne ich auch die Konzepte von Atomic, STM und async recht gut, in C++ muss ich mich da allerdings nochmal richtig durchwühlen (wie bei vielen anderen dingen auch).
Sorry, aber das klingt alles nicht so als ob du genügend Grundlagen/Vorkenntnisse hättest um sowas erfolgreich auf die Beine zu stellen.
Das Projekt ist ja auch sowas wie mein "mal schauen was ich hinkrieg". Wenns nicht nach meinen Vorstellungen machbar ist muss ich eben auf ein anderes Konzept zurückgreifen. Ich hab aber nunmal mit Locks in Java (ich kenne eigentlich bisher Mutex und Lock nur synonym) relativ schlechte Erfahrungen gemacht. Nachdem ich mit Java angefangen hatte hab ich mich ne Weile mit nem Websever Projekt von nem Bekannten beschäftigt, das ist quasi ständig in nen Deadlock gerannt, irgendwann hat dann der Bekannte auch aufgehört und nochmal von vorne angefangen (da hatte ich schon lange keinen Nerf mehr). Clojure, das ja auch auf der JVM läuft und mit Java voll kompatibel ist, hat die Probleme nicht. Daten sind per se Immutable, es gibt standartmäßig keine Variablen, ändere ich eine Datenstruktur wird eine Kopie erzeugt. Um Variable Werte speichern zu können muss ich da auf Atomics, Agents oder Refs (transactional references) zurückgreifen, welche alle Thread safe sind und nicht blockieren. Clojure wurde auch mit dem Ziel entwickelt weshalbs auch relativ flott ist. Im vergleich zu anderen HTTP Servern ist der code von Clojure-Ring wohl desshalb auch zimlich einfach gehalten.
Sorry fuer den langen Text, aber ich wollte nur sagen das ich mit Concurrency in anderen Sprachen schon einiges zu tun hatte. Ob und wie sich das auf C++ übertragen lässt wird sich zeigen. Da die Benchmarks zum Thema Nebenläufigkeit in C++ die ich kenne (vor allem auf http://www.grimm-jaud.de) zimlich ernüchternd ausfallen, habe ich beschlossen innerhalb meiner Systeme weitgehend darauf zu verzichten, es hat immer nur ein Thread gleichzeitig Zugriff auf einen Verbund von Systemen (das steuert der Eventmanager), eine Ausnahme ist atm nur die Eventqueue in die von aussen gepusht werden kann, und das Positioning System, dessen variabler Teil vom Eventmanager beschrieben wird.
Vielleicht kennt ja zufällig jemand nen guten link zum Thema multithreading Performance, habe leider bisher wenig gefunden, http://www.grimm-jaud.de ist da leider auch nicht besonders aussagekräftig.