2 Stimmen

Welche shared_ptr Politik mit asynchronen Schema zu verwenden?

Ich habe meinen eigenen Multithreading-Dienst, der einige Befehle verarbeitet. Der Dienst besteht aus einem Befehlsparser, Worker-Threads mit Warteschlangen und einigen Caches. Ich möchte nicht den Lebenszyklus jedes Objekts im Auge behalten, daher verwende ich shared_ptr's sehr extensiv. Jede Komponente verwendet shared_ptr's auf ihre eigene Weise:

  • Der Befehlsparser erstellt shared_ptr's und speichert sie im Cache;
  • worker bindet shared_ptr's an Funktoren und stellt sie in die Warteschlange.
  • Cache vorübergehend oder dauerhaft einige shared_ptr's enthält.
  • die Daten, auf die von shared_ptr verwiesen wird, können auch andere shared_ptr's enthalten.

Und es gibt einen weiteren zugrundeliegenden Dienst (z.B. Befehlsempfänger und -sender), der die gleiche Struktur hat, aber seinen eigenen Cache, Worker und shared_ptr's verwendet. Es ist unabhängig von meinem Dienst und wird von einem anderen Entwickler gepflegt.

Es ist ein kompletter Albtraum, wenn ich versuche, alle shared_ptr-Abhängigkeiten zu verfolgen, um Querverweise zu vermeiden.

Gibt es eine Möglichkeit, einige shared_ptr "Schnittstelle" oder "Politik" zu spezifizieren, so werde ich wissen, welche shared_ptr's ich sicher an den zugrunde liegenden Dienst übergeben kann, ohne den Code zu inspizieren oder mit dem Entwickler zu interagieren? Die Richtlinie sollte den Besitzzyklus von shared_ptr einbeziehen, z. B. hält der Worker den Funktor mit der gebundenen shared_ptr seit dem Aufruf der dispatch()-Funktion und nur bis zu einem anderen Funktionsaufruf, während der Cache die shared_ptr seit dem Konstruktoraufruf des Cache und bis zum Destruktoraufruf des Cache hält.

Insbesondere bin ich neugierig auf die Situation beim Herunterfahren, wenn die Anwendung einfriert, während sie darauf wartet, dass die Threads beitreten.

1voto

Matthieu M. Punkte 266317

Es gibt kein Patentrezept... und shared_ptr ist sicherlich keine.


Meine erste Frage wäre: Brauchen Sie all diese gemeinsamen Zeiger?

Der beste Weg, um zyklische Verweise zu vermeiden, besteht darin, die Lebenszeitrichtlinien der einzelnen Objekte zu definieren und sicherzustellen, dass sie kompatibel sind. Dies kann leicht dokumentiert werden:

  • Wenn Sie mir eine Referenz übergeben, erwarte ich, dass das Objekt während des gesamten Funktionsaufrufs weiterlebt, aber nicht mehr
  • Sie reichen mir eine unique_ptr Ich bin jetzt für das Objekt verantwortlich.
  • Sie reichen mir eine shared_ptr Ich erwarte, dass ich das Objekt selbst in der Hand behalten kann, ohne Sie zu beeinträchtigen.

Nun, es gibt selten Situationen, in denen der Einsatz von shared_ptr ist in der Tat notwendig. Der Hinweis auf Caches lässt mich vermuten, dass dies bei Ihnen der Fall sein könnte, zumindest für algunos verwendet.

In diesem Fall können Sie (zumindest informell) einen mehrschichtigen Ansatz durchsetzen.

  • Definieren Sie eine Anzahl von Ebenen, von 0 (die Basis) auf unendlich
  • Jede Typ eines Objekts einem Layer zugeordnet ist, können sich mehrere Typen denselben Layer teilen
  • Ein Objekt vom Typ A vielleicht nur eine shared_ptr auf ein Objekt des Typs B wenn, und nur wenn, Layer(A) > Layer(B)

Beachten Sie, dass wir Geschwisterbeziehungen ausdrücklich untersagen. Mit diesem Schema kann niemals ein Kreis von Referenzen gebildet werden. Vielmehr erhalten wir einen DAG (Directed Acyclic Graph).

Wenn nun ein Typ erstellt wird, muss ihm eine Ebenennummer zugewiesen werden, und dies muss dokumentiert werden (vorzugsweise im Code).

Ein Objekt kann ändern der Ebene jedoch nicht:

  • wenn sich die Zahl der Ebenen verringert, müssen Sie die Referenzen, die sie enthalten, erneut überprüfen (einfach)
  • wenn sich die Zahl der Schichten erhöht, müssen Sie alle Verweise auf diese Schicht erneut überprüfen (schwer)

Hinweis: Objekttypen, die keinen Verweis enthalten können, befinden sich üblicherweise in der Ebene 0 .

Anmerkung 2: Ich bin zum ersten Mal über diese Konvention in einem Artikel von Herb Sutter gestolpert, in dem er sie auf Mutexe anwendet und versucht, Deadlocks zu verhindern. Dies ist eine Anpassung an das aktuelle Thema.


Dies kann etwas automatischer (durch den Compiler) erzwungen werden, solange Sie bereit sind, Ihre bestehende Codebasis zu bearbeiten.

Wir erstellen eine neue SharedPtr Klasse, die unser Schichtenschema kennt:

template <typename T>
constexpr unsigned getLayer(T const&) { return T::Layer; }

template <typename T, unsigned L>
class SharedPtrImpl {
public:
  explicit SharedPtrImpl(T* t): _p(t)
  {
    static_assert(L > getLayer(std::declval<T>()), "Layering Violation");
  }

  T* get() const { return _p.get(); }

  T& operator*() const { return *this->get(); }
  T* operator->() const { return this->get(); }

private:
  std::shared_ptr<T> _p;
};

Jede Art, die in einer solchen Anlage gehalten werden kann SharedPtr wird seine Schicht statisch zugewiesen, und wir verwenden eine Basisklasse, um uns zu helfen:

template <unsigned L>
struct LayerMember {
  static unsigned const Layer = L;

  template <typename T>
  using SharedPtr<T> = SharedPtrImpl<T, L>;
};

Und jetzt können wir sie einfach nutzen:

class Foo: public LayerMember<3> {
public:

private:
  SharedPtr<Bar> _bar; // statically checked!
};

Allerdings ist dieser Kodierungsansatz etwas aufwändiger, ich denke, dass die Konvention durchaus ausreichen kann ;)

0voto

John Zwinck Punkte 221200

Sie sollten sich weak_ptr ansehen. Es ergänzt shared_ptr, aber nicht halten Objekte lebendig, so ist sehr nützlich, wenn Sie zirkuläre Referenzen haben könnte.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X