Ich habe einen Wrapper für einen alten Code.
class A{
L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
A(A const&) = delete;
L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
... // proper resource management here
};
In diesem Legacy-Code ist die Funktion, die ein Objekt „dupliziert“, nicht threadsicher (wenn dasselbe erste Argument aufgerufen wird), daher wird sie const
im Wrapper nicht markiert . Ich schätze folgende moderne Regeln: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
Dies duplicate
scheint eine gute Möglichkeit zu sein, einen Kopierkonstruktor zu implementieren, mit Ausnahme der Details, die es nicht sind const
. Deshalb kann ich das nicht direkt machen:
class A{
L* impl_; // the legacy object has to be in the heap
A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Was ist der Ausweg aus dieser paradoxen Situation?
(Nehmen wir auch an, dass dies legacy_duplicate
nicht threadsicher ist, aber ich weiß, dass das Objekt beim Beenden im ursprünglichen Zustand bleibt. Als C-Funktion ist das Verhalten nur dokumentiert, hat aber kein Konzept der Konstanz.)
Ich kann mir viele mögliche Szenarien vorstellen:
(1) Eine Möglichkeit besteht darin, dass es überhaupt keine Möglichkeit gibt, einen Kopierkonstruktor mit der üblichen Semantik zu implementieren. (Ja, ich kann das Objekt bewegen und das ist nicht das, was ich brauche.)
(2) Andererseits ist das Kopieren eines Objekts von Natur aus nicht threadsicher in dem Sinne, dass das Kopieren eines einfachen Typs die Quelle in einem halbmodifizierten Zustand finden kann, also kann ich einfach weitermachen und dies vielleicht tun,
class A{
L* impl_;
A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(3) oder deklarieren Sie einfach duplicate
const und lügen Sie über Thread-Sicherheit in allen Kontexten. (Schließlich kümmert sich die Legacy-Funktion nicht darum, const
sodass sich der Compiler nicht einmal beschwert.)
class A{
L* impl_;
A(A const& other) : L{other.duplicate()}{}
L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
(4) Schließlich kann ich der Logik folgen und einen Kopierkonstruktor erstellen, der ein Nicht-Konstanten- Argument verwendet.
class A{
L* impl_;
A(A const&) = delete;
A(A& other) : L{other.duplicate()}{}
L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};
Es stellt sich heraus, dass dies in vielen Kontexten funktioniert, da diese Objekte normalerweise nicht funktionieren const
.
Die Frage ist, ist dies eine gültige oder gemeinsame Route?
Ich kann sie nicht benennen, aber ich erwarte intuitiv viele Probleme auf dem Weg zu einem nicht konstanten Kopierkonstruktor. Wahrscheinlich wird es aufgrund dieser Subtilität nicht als Werttyp qualifiziert.
(5) Obwohl dies ein Overkill zu sein scheint und hohe Laufzeitkosten verursachen könnte, könnte ich einen Mutex hinzufügen:
class A{
L* impl_;
A(A const& other) : L{other.duplicate_locked()}{}
L* duplicate(){
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
L* duplicate_locked() const{
std::lock_guard<std::mutex> lk(mut);
L* ret; legacy_duplicate(impl_, &ret); return ret;
}
mutable std::mutex mut;
};
Aber dazu gezwungen zu sein, sieht nach Pessimisierung aus und macht die Klasse größer. Ich bin mir nicht sicher. Ich neige derzeit zu (4) oder (5) oder einer Kombination aus beiden.
- BEARBEITEN
Andere Option:
(6) Vergessen Sie den Unsinn der doppelten Elementfunktion und rufen Sie einfach legacy_duplicate
vom Konstruktor auf und erklären Sie, dass der Kopierkonstruktor nicht threadsicher ist. (Und wenn nötig, machen Sie eine andere thread-sichere Version des Typs. A_mt
)
class A{
L* impl_;
A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};
BEARBEITEN 2
Dies könnte ein gutes Modell für die Funktionsweise der Legacy-Funktion sein. Beachten Sie, dass der Aufruf durch Berühren der Eingabe in Bezug auf den durch das erste Argument dargestellten Wert nicht threadsicher ist.
void legacy_duplicate(L* in, L** out){
*out = new L{};
char tmp = in[0];
in[0] = tmp;
std::memcpy(*out, in, sizeof *in); return;
}
L
der durch Erstellen einer neuenL
Instanz geändert wird ? Wenn nicht, warum glauben Sie, dass dieser Vorgang nicht threadsicher ist?legacy_duplicate
nicht mit demselben ersten Argument aus zwei verschiedenen Threads aufgerufen werden.const
wirklich bedeutet. :-) Ich würde nicht zweimal darüber nachdenken, einenconst&
in meine Kopie aufzunehmen, solange ich nichts ändereother
. Ich denke immer an Thread-Sicherheit als etwas, das man zusätzlich zu dem hinzufügt, auf das über mehrere Kapseln über Kapselung zugegriffen werden muss, und ich freue mich sehr auf die Antworten.Antworten:
Ich würde nur Ihre beiden Optionen (4) und (5) einschließen, mich aber explizit für thread-unsicheres Verhalten entscheiden, wenn Sie der Meinung sind, dass dies für die Leistung erforderlich ist.
Hier ist ein vollständiges Beispiel.
Ausgabe:
Dies folgt dem Google Style Guide, in dem die
const
Thread-Sicherheit kommuniziert wird. Code, der Ihre API aufruft, kann sich jedoch mit deaktivierenconst_cast
quelle
legacy_duplicate
könnte seinvoid legacy_duplicate(L* in, L** out) { *out = new L{}; char tmp = in[0]; /*some weird call here*/; in[0] = tmp; std::memcpy(*out, in, sizeof *in); return; }
(dh nicht-constin
)A a2(a1)
könnte versuchen, threadsicher zu sein (oder gelöscht zu werden) undA a2(const_cast<A&>(a1))
würde überhaupt nicht versuchen, threadsicher zu sein.A
sowohl in thread-sicheren als auch in thread-unsicheren Kontexten zu verwenden, sollten Sie denconst_cast
auf den aufrufenden Code ziehen, damit klar ist, wo bekanntermaßen die Thread-Sicherheit verletzt wird. Es ist in Ordnung, zusätzliche Sicherheit hinter die API (Mutex) zu stellen, aber nicht in Ordnung, die Unsicherheit zu verbergen (const_cast).TLDR: Entweder die Umsetzung Ihrer Doppelfunktion zu beheben, oder ein Mutex einführen (oder eine geeignetere Verriegelungsvorrichtung, vielleicht ein spinlock, oder stellen Sie sicher , Ihre Mutex Spin konfiguriert ist , bevor etwas schwerer tun) für jetzt , beheben dann die Umsetzung der Vervielfältigung und entfernen Sie die Verriegelung, wenn die Verriegelung tatsächlich zu einem Problem wird.
Ich denke, ein wichtiger Punkt ist, dass Sie eine Funktion hinzufügen, die vorher nicht existierte: die Möglichkeit, ein Objekt aus mehreren Threads gleichzeitig zu duplizieren.
Unter den von Ihnen beschriebenen Bedingungen wäre dies natürlich ein Fehler gewesen - eine Rennbedingung, wenn Sie dies zuvor getan hätten, ohne irgendeine externe Synchronisation zu verwenden.
Daher wird jede Verwendung dieser neuen Funktion etwas, das Sie Ihrem Code hinzufügen und nicht als vorhandene Funktionalität erben. Sie sollten derjenige sein, der weiß, ob das Hinzufügen der zusätzlichen Sperre tatsächlich kostspielig ist - je nachdem, wie oft Sie diese neue Funktion verwenden werden.
Aufgrund der wahrgenommenen Komplexität des Objekts - aufgrund der speziellen Behandlung, die Sie ihm geben - gehe ich davon aus, dass das Duplizierungsverfahren nicht trivial ist und daher in Bezug auf die Leistung bereits recht teuer ist.
Basierend auf dem oben Gesagten haben Sie zwei Wege, denen Sie folgen können:
A) Sie wissen, dass das Kopieren dieses Objekts aus mehreren Threads nicht oft genug erfolgt, um den Aufwand für die zusätzliche Sperrung zu erhöhen - möglicherweise trivial billig, zumindest angesichts der Tatsache, dass das vorhandene Duplizierungsverfahren allein teuer genug ist, wenn Sie a verwenden Spinlock / Pre-Spinning-Mutex, und es gibt keinen Streit darüber.
B) Sie vermuten, dass das Kopieren von mehreren Threads häufig genug erfolgt, damit das zusätzliche Sperren ein Problem darstellt. Dann haben Sie wirklich nur eine Option - korrigieren Sie Ihren Duplizierungscode. Wenn Sie das Problem nicht beheben, müssen Sie es trotzdem sperren, sei es auf dieser Abstraktionsebene oder anderswo, aber Sie benötigen es, wenn Sie keine Fehler möchten - und wie wir in diesem Pfad festgestellt haben, nehmen Sie an Dieses Sperren ist zu kostspielig. Daher besteht die einzige Möglichkeit darin, den Duplizierungscode zu korrigieren.
Ich vermute, dass Sie sich wirklich in Situation A befinden und nur einen Spinlock / Spinning-Mutex hinzufügen, der im unbestrittenen Zustand nahezu keine Leistungseinbußen aufweist, funktioniert einwandfrei (denken Sie jedoch daran, ihn zu bewerten).
Theoretisch gibt es eine andere Situation:
C) Im Gegensatz zu der scheinbaren Komplexität der Duplizierungsfunktion ist sie eigentlich trivial, kann aber aus irgendeinem Grund nicht behoben werden. Es ist so trivial, dass selbst ein unbestrittener Spinlock zu einer inakzeptablen Leistungsverschlechterung bei der Duplizierung führt. Duplikate auf Parallell-Threads werden selten verwendet. Die Duplizierung auf einem einzelnen Thread wird ständig verwendet, wodurch die Leistungsverschlechterung absolut inakzeptabel wird.
In diesem Fall schlage ich Folgendes vor: Deklarieren Sie die Standardkopierkonstruktoren / -operatoren als gelöscht, um zu verhindern, dass jemand sie versehentlich verwendet. Erstellen Sie zwei explizit aufrufbare Duplizierungsmethoden, eine thread-sichere und eine thread-unsichere. Lassen Sie Ihre Benutzer sie je nach Kontext explizit aufrufen. Auch hier gibt es keine andere Möglichkeit, eine akzeptable Single-Thread-Leistung und ein sicheres Multi-Threading zu erzielen, wenn Sie sich wirklich in dieser Situation befinden und die vorhandene Duplizierungsimplementierung einfach nicht reparieren können . Aber ich halte es für sehr unwahrscheinlich, dass Sie es wirklich sind.
Fügen Sie einfach diesen Mutex / Spinlock und Benchmark hinzu.
quelle
std::mutex
? Die Duplikatfunktion ist kein Geheimnis, ich habe sie nicht erwähnt, um das Problem auf einem hohen Niveau zu halten und keine Antworten über MPI zu erhalten. Aber da Sie so tief gegangen sind, kann ich Ihnen mehr Details geben. Die Legacy-Funktion istMPI_Comm_dup
und die effektive Sicherheit ohne Thread wird hier beschrieben (ich habe es bestätigt). Github.com/pmodels/mpich/issues/3234 . Aus diesem Grund kann ich Duplikate nicht reparieren. (Wenn ich einen Mutex hinzufüge, werde ich versucht sein, alle MPI-Aufrufe threadsicher zu machen.)