Code review von synchronized queue
-
Ich bastle an einer synchronized queue. Der Code ist hier http://ideone.com/17IYk6 .
#ifndef SYNC_H #define SYNC_H #include <basic/queue.h> #include <boost/thread.hpp> namespace af { namespace sync { template<typename T> struct queue { // for convenient typedef typename af::basic::queue<T> container; struct monitor_t { monitor_t() : q_(0) {}; monitor_t(monitor_t& m) : q_(m.q_) {} ~monitor_t() {} template<typename F> void operator()(F& f) const { boost::lock_guard<boost::mutex> lock(q_->m_); q_->c_.notify_one(); f(q_->q_); } private: queue* q_; // for setting reference friend monitor_t queue::monitor(); }; queue() : q_() {} bool empty() const { boost::lock_guard<boost::mutex> lock(m_); return q_.empty(); } size_t size() const { boost::lock_guard<boost::mutex> lock(m_); return q_.size(); } bool pop(T& msg, unsigned int ms) { boost::system_time const timeout = boost::get_system_time() + boost::posix_time::milliseconds(ms); boost::unique_lock<boost::mutex> lock(m_); while( q_.empty()) { if (!c_.timed_wait(lock, timeout)) return false; } q_.pop(msg); return true; } // consumes msg and moves it into the queue void push(T& msg) { boost::lock_guard<boost::mutex> lock(m_); q_.push(msg); c_.notify_one(); } void clear() { boost::lock_guard<boost::mutex> lock(m_); q_.clear(); } monitor_t monitor() { monitor_t tmp; tmp.q_ = this; return tmp; } ~queue() { clear(); } private: // do not copy queue(queue const&); queue& operator=(queue const&); af::basic::queue<T> q_; mutable boost::mutex m_; boost::condition_variable c_; }; } }Bei der Implementierung folge ich dem Beispiel aus http://www.youtube.com/playlist?list=PL1835A90FC78FF8BE . Aber da ich ab und an mehr als nur pop und push einzeln aufrufen moechte sondern als "Transaktion", kann ich bei Bedarf ein Monitorobjekt benutzen. Dabei folge ich dem Beispiel monitor<T> aus http://isocpp.org/blog/2013/01/c-concurrency-herb-sutter (sollte in die Playlist) angepasst auf meine Queue.
Nun zu meinem Anliegen: Gibt es etwas zu verbessern oder zu aendern, sind Fehler enthalten? Fuer Kommentare bin ich dankbar. Das public Interface von af::basic::queue weicht von std::queue etwas ab und sieht folgendermassen aus:
template<typename T> struct queue { queue(); bool empty() const; size_t size() const; void pop(T& msg); void push(T& msg); void clear(); ~queue(); };Es kompiliert gcc 4.72 und einfache Tests sind erfolgreich. Ich kann kein C++11 benutzen und bei boost bin ich auf Version 1.49.0 beschraenkt. Danke.
edit: code von ideone hierhergetragen.
-
-
Warum unique_lock in pop() ?
Ansonsten ist mir das notify_one in monitor nicht ganz geheuer. Aber das ist nur ein Gefühl ohne dass ich es belegen kann. Ich habe das Video von Herb Sutter dazu jetzt aber auch nicht gesehen.
Eine Optimierungsmöglichkeit wären 2 Arten von Locks: read und write. size() und empty() brauchen nur readlocks während pop und push write Locks brauchen. Ob das eine optimierung ist, weiß ich aber nicht. Es wäre aber auf jedenfall eine Option die man sich anschauen könnte.
uU wäre ein non blocking pop auch interessant. Also eins ohne timed_wait. Wobei man das wahrscheinlich mit einem 0 als Parameter simulieren könnte. Gefühlt könnte man hier aber mit einem eigenen empty test in pop etwas rausholen.
Sieht aber eigentlich alles OK aus.
-
Ich mag keine absoluten Timeouts. Ist zwar (viel) einfacher "korrekt" zum implementieren als mit relativen (vor allem weil die Standard-Library keine "timeout clock" Klasse zur Verfügung stellt -- oder gibt es sowas?), aber dafür ist es eben auch nur "korrekt" und nicht korrekt: bei Änderungen der Systemzeit kann es zu Problemen kommen.
~~
Und die Monitor-Sache könnte problematisch werden, wenn die "gemonitorte" Funktion [c]pop[/c] aufruft. Dann wäre die Mutex nämlich doppelt gelockt, und könnte daher durch das [c]timed_lock[/c] nicht mehr entsperrt werden.Eine Möglichkeit das zu lösen wäre z.B. direkten Zugriff auf die Mutex zu geben, und einen pop-Overload zu machen der ein [c]unique_lock[/c] Objekt als Parameter nimmt.
Oder die Queue selbst lockbar machen, so dass man ein [c]unique_lock<my_queue>[/c] verwenden kann.
Oder dem Funktor den der Monitor ausführt als Parameter das [c]unique_lock[/c] mitgeben - irgendsowas.~~
EDIT: ich sehe gerade du übergibst ja direkt ne Referenz auf die interne "nicht threadsafe" queue an die Funktion. Dadurch gibt es das von mir oben beschriebene Problem natürlich nicht mehr.
-
Shade Of Mine schrieb:
Warum unique_lock in pop() ?
Vermutlich weil man einen
lock_guardnicht "entlocken" kann (oder geht das?) -- siehetimed_lockAufruf.
-
Shade Of Mine schrieb:
Ansonsten ist mir das notify_one in monitor nicht ganz geheuer.
Ja, da müsste wohl ein
notify_allhin.
Denn die aufgerufene Funktion kann ja mehr als einen Eintrag einfügen. Es würde dann aber nur einer der "popper" aufgeweckt, und das ist vermutlich nicht im Sinne des Erfinders.
-
Ich habe das Video von Herb Sutter dazu jetzt aber auch nicht gesehen.
Kommt auch so nicht vor, da es im Video sehr einfach gehalten wurde.
hustbaer schrieb:
Ja, da müsste wohl ein
notify_allhin.
Denn die aufgerufene Funktion kann ja mehr als einen Eintrag einfügen. Es würde dann aber nur einer der "popper" aufgeweckt, und das ist vermutlich nicht im Sinne des Erfinders.Korrekt, das wird im Monitor geaendert. Wie sieht das bei push aus, wenn 2 Elemente mittels push ohne Kontextwechsel eingefuegt werden und zwei Konsumer warten. Wird dann auch nur einer aufgeweckt? Ich glaube da muss ich nochmal im Standard zu pthread und boost nachlesen.
wäre ein non blocking pop auch interessant. Also eins ohne timed_wait.
Ja, darueber habe ich auch nachgedacht. Wollte das Interface mit
try_pop(non blocking) undpop(blockierend ohne timeout) nicht ueberladen.Ich mag keine absoluten Timeouts.
Ich auch nicht. Aber das C++11 Threadinterface mit
wait_forsteht mir leider nicht zur Verfuegung. Somit wird das relative Timeout am Interface in ein absolutes gewandelt. Der Benutzer der Klasse sieht nur das relative. Und ja, Systemzeit aendern ist fatal.Warum unique_lock in pop() ?
Das Interface von
boost::condition_variableaber auch von C++11sstd::condition_variablesieht es vor. Und ja,lock_guardhat nur Konstruktor und Destruktor, kann also nicht unlocken.Eine Optimierungsmöglichkeit wären 2 Arten von Locks: read und write. size() und empty() brauchen nur readlocks während pop und push write Locks brauchen.
Darueber werde ich nachdenken.
Dann wäre die Mutex nämlich doppelt gelockt,
Ja, ich haette ein Beispiel fuer die Benutzung angeben sollen. Die Funktion/Funktor bekommt ein
af::sync::queue<T>::container(also hieraf::basic::queue<T>) und nichtaf::sync::queue<T>.Auf alle Faelle: Ich danke euch!
-
Nachtrag: Ich habe mich entschieden das Monitorobjekt aufzugeben und einfach eine kleine Methode
synchronized(F f)anzubieten, die das gleiche macht.template<typename F> void synchronized(F& f) { boost::lock_guard<boost::mutex> lock(m_); c_.notify_all(); f(q_); }
-
knivil schrieb:
Korrekt, das wird im Monitor geaendert. Wie sieht das bei push aus, wenn 2 Elemente mittels push ohne Kontextwechsel eingefuegt werden und zwei Konsumer warten. Wird dann auch nur einer aufgeweckt? Ich glaube da muss ich nochmal im Standard zu pthread und boost nachlesen.
Ich bin mir 95% sicher dass das kein Problem macht.
Aber lies es im Standard nach, garantieren kann ich es dir nicht.Falls der Standard doch doof sein sollte kann man als Workaround den aufgeweckten Popper vor dem Unlock nochmal checken lassen ob die Queue noch Elemente enthält, und falls ja nochmal
notify_oneaufrufen lassen.In Fällen wo immer viele Popper warten, aber die neuen Einträge immer nur langsam dahertröpfeln könnte das performanceschonender sein als ein notify_all.
(Wobei es natürlich in Fällen wo nur ein Popper wartet sehr gute Chancen hat eine Spur langsamer zu sein.)
-
Falls es da immer noch um die Message Queue von hier geht und du durch diese Queue einfach nur Threads mit Arbeit versorgen willst, möchte ich auch mal die Möglichkeit in den Raum werfen, die Queue völlig lock-free als einfachen Ring Buffer zu implementieren.