Ist von Natur aus std::mutex
weder beweglich noch kopierkonstruierbar. Dies bedeutet, dass eine Klasse A
, die einen Mutex enthält, keinen Standard-Verschiebungskonstruktor erhält.
Wie würde ich diesen Typ A
fadensicher beweglich machen ?
Ist von Natur aus std::mutex
weder beweglich noch kopierkonstruierbar. Dies bedeutet, dass eine Klasse A
, die einen Mutex enthält, keinen Standard-Verschiebungskonstruktor erhält.
Wie würde ich diesen Typ A
fadensicher beweglich machen ?
std::lock_guard
Methode.
Antworten:
Beginnen wir mit ein bisschen Code:
class A
{
using MutexType = std::mutex;
using ReadLock = std::unique_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
mutable MutexType mut_;
std::string field1_;
std::string field2_;
public:
...
Ich habe dort einige eher suggestive Typ-Aliase eingefügt, die wir in C ++ 11 nicht wirklich nutzen, aber in C ++ 14 viel nützlicher werden. Sei geduldig, wir werden dort ankommen.
Ihre Frage lautet:
Wie schreibe ich den Verschiebungskonstruktor und den Verschiebungszuweisungsoperator für diese Klasse?
Wir beginnen mit dem Verschiebungskonstruktor.
Konstruktor verschieben
Beachten Sie, dass das Mitglied mutex
erstellt wurde mutable
. Genau genommen ist dies für die Umzugsmitglieder nicht erforderlich, aber ich gehe davon aus, dass Sie auch Kopiermitglieder möchten. Ist dies nicht der Fall, muss der Mutex nicht erstellt werden mutable
.
Beim Konstruieren A
müssen Sie nicht sperren this->mut_
. Sie müssen jedoch mut_
das Objekt sperren, aus dem Sie erstellen (verschieben oder kopieren). Dies kann folgendermaßen geschehen:
A(A&& a)
{
WriteLock rhs_lk(a.mut_);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
Beachten Sie, dass wir die Mitglieder von this
zuerst standardmäßig erstellen und ihnen dann erst Werte zuweisen mussten, nachdem sie a.mut_
gesperrt wurden.
Zuordnung verschieben
Der Verschiebungszuweisungsoperator ist wesentlich komplizierter, da Sie nicht wissen, ob ein anderer Thread auf die lhs oder rhs des Zuweisungsausdrucks zugreift. Im Allgemeinen müssen Sie sich vor dem folgenden Szenario schützen:
// Thread 1
x = std::move(y);
// Thread 2
y = std::move(x);
Hier ist der Verschiebungszuweisungsoperator, der das obige Szenario korrekt schützt:
A& operator=(A&& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
WriteLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = std::move(a.field1_);
field2_ = std::move(a.field2_);
}
return *this;
}
Beachten Sie, dass Sie std::lock(m1, m2)
die beiden Mutexe sperren müssen , anstatt sie nur nacheinander zu sperren. Wenn Sie sie nacheinander sperren, können Sie einen Deadlock erhalten, wenn zwei Threads zwei Objekte in entgegengesetzter Reihenfolge wie oben gezeigt zuweisen. Es std::lock
geht darum, diesen Deadlock zu vermeiden.
Konstruktor kopieren
Sie haben nicht nach den Kopienmitgliedern gefragt, aber wir könnten genauso gut jetzt darüber sprechen (wenn nicht Sie, wird jemand sie brauchen).
A(const A& a)
{
ReadLock rhs_lk(a.mut_);
field1_ = a.field1_;
field2_ = a.field2_;
}
Der Kopierkonstruktor ReadLock
ähnelt stark dem Verschiebungskonstruktor, außer dass der Alias anstelle des verwendet wird WriteLock
. Derzeit sind diese beiden Alias std::unique_lock<std::mutex>
und so macht es keinen wirklichen Unterschied.
In C ++ 14 haben Sie jedoch die Möglichkeit, Folgendes zu sagen:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
Dies kann eine Optimierung sein, aber nicht definitiv. Sie müssen messen, um festzustellen, ob dies der Fall ist. Mit dieser Änderung kann man jedoch Konstrukte aus denselben rhs in mehreren Threads gleichzeitig kopieren . Die C ++ 11-Lösung zwingt Sie, solche Threads sequentiell zu machen, obwohl die rhs nicht geändert werden.
Zuordnung kopieren
Der Vollständigkeit halber hier der Kopierzuweisungsoperator, der nach dem Lesen von allem anderen ziemlich selbsterklärend sein sollte:
A& operator=(const A& a)
{
if (this != &a)
{
WriteLock lhs_lk(mut_, std::defer_lock);
ReadLock rhs_lk(a.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1_ = a.field1_;
field2_ = a.field2_;
}
return *this;
}
Und soweiter und sofort.
Alle anderen Mitglieder oder freien Funktionen, die auf A
den Status zugreifen , müssen ebenfalls geschützt werden, wenn Sie erwarten, dass mehrere Threads sie gleichzeitig aufrufen können. Zum Beispiel hier swap
:
friend void swap(A& x, A& y)
{
if (&x != &y)
{
WriteLock lhs_lk(x.mut_, std::defer_lock);
WriteLock rhs_lk(y.mut_, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
using std::swap;
swap(x.field1_, y.field1_);
swap(x.field2_, y.field2_);
}
}
Beachten Sie, dass, wenn Sie nur von std::swap
der std::swap
Ausführung des Auftrags abhängig sind , die Sperrung in der falschen Granularität erfolgt und zwischen den drei Bewegungen, die intern ausgeführt werden , gesperrt und entsperrt wird .
Wenn Sie darüber nachdenken, erhalten swap
Sie möglicherweise einen Einblick in die API, die Sie möglicherweise benötigen, um eine "thread-sichere" API bereitzustellen A
, die sich aufgrund des Problems der "Sperrgranularität" im Allgemeinen von einer "nicht thread-sicheren" API unterscheidet.
Beachten Sie auch die Notwendigkeit, sich vor "Selbsttausch" zu schützen. "Selbsttausch" sollte ein No-Op sein. Ohne den Selbsttest würde man denselben Mutex rekursiv sperren. Dies könnte auch ohne Selbsttest mit std::recursive_mutex
for gelöst werden MutexType
.
Aktualisieren
In den Kommentaren unten ist Yakk ziemlich unglücklich darüber, dass er standardmäßig Dinge in den Kopier- und Verschiebungskonstruktoren konstruieren muss (und er hat einen Punkt). Wenn Sie sich zu diesem Thema so stark fühlen, dass Sie bereit sind, sich daran zu erinnern, können Sie es folgendermaßen vermeiden:
Fügen Sie alle Sperrtypen hinzu, die Sie als Datenelemente benötigen. Diese Mitglieder müssen vor den zu schützenden Daten stehen:
mutable MutexType mut_;
ReadLock read_lock_;
WriteLock write_lock_;
// ... other data members ...
Und dann tun Sie dies in den Konstruktoren (z. B. dem Kopierkonstruktor):
A(const A& a)
: read_lock_(a.mut_)
, field1_(a.field1_)
, field2_(a.field2_)
{
read_lock_.unlock();
}
Hoppla, Yakk hat seinen Kommentar gelöscht, bevor ich die Gelegenheit hatte, dieses Update abzuschließen. Aber er verdient Anerkennung dafür, dass er dieses Problem vorangetrieben und eine Lösung für diese Antwort gefunden hat.
Update 2
Und dyp kam auf diesen guten Vorschlag:
A(const A& a)
: A(a, ReadLock(a.mut_))
{}
private:
A(const A& a, ReadLock rhs_lk)
: field1_(a.field1_)
, field2_(a.field2_)
{}
mutexes
in Klassentypen ist nicht der " einzig wahre Weg". Es ist ein Werkzeug in der Toolbox, und wenn Sie es verwenden möchten, gehen Sie wie folgt vor.
Angesichts der Tatsache, dass es keinen guten, sauberen und einfachen Weg gibt, dies zu beantworten - Antons Lösung ist meiner Meinung nach richtig, aber definitiv umstritten. Wenn keine bessere Antwort gefunden wird, würde ich empfehlen, eine solche Klasse auf den Haufen zu legen und sich darum zu kümmern über a std::unique_ptr
:
auto a = std::make_unique<A>();
Es ist jetzt ein voll beweglicher Typ und jeder, der den internen Mutex während einer Bewegung sperrt, ist immer noch sicher, auch wenn es fraglich ist, ob dies eine gute Sache ist
Wenn Sie eine Kopiersemantik benötigen, verwenden Sie einfach
auto a2 = std::make_shared<A>();
Dies ist eine verkehrte Antwort. Anstatt "Diese Objekte müssen synchronisiert werden" als Basis des Typs einzubetten, fügen Sie sie stattdessen unter einen beliebigen Typ ein.
Sie gehen sehr unterschiedlich mit einem synchronisierten Objekt um. Ein großes Problem ist, dass Sie sich um Deadlocks sorgen müssen (Sperren mehrerer Objekte). Grundsätzlich sollte es auch niemals Ihre "Standardversion eines Objekts" sein: Synchronisierte Objekte sind für Objekte gedacht, die in Konflikt geraten, und Ihr Ziel sollte es sein, Konflikte zwischen Threads zu minimieren und nicht unter den Teppich zu kehren.
Das Synchronisieren von Objekten ist jedoch weiterhin nützlich. Anstatt von einem Synchronisierer zu erben, können wir eine Klasse schreiben, die einen beliebigen Typ in die Synchronisation einschließt. Benutzer müssen jetzt, da es synchronisiert ist, durch einige Rahmen springen, um Operationen an dem Objekt auszuführen, aber sie sind nicht auf einige handcodierte begrenzte Operationen an dem Objekt beschränkt. Sie können mehrere Operationen für das Objekt zu einer zusammensetzen oder eine Operation für mehrere Objekte ausführen.
Hier ist ein synchronisierter Wrapper um einen beliebigen Typ T
:
template<class T>
struct synchronized {
template<class F>
auto read(F&& f) const&->std::result_of_t<F(T const&)> {
return access(std::forward<F>(f), *this);
}
template<class F>
auto read(F&& f) &&->std::result_of_t<F(T&&)> {
return access(std::forward<F>(f), std::move(*this));
}
template<class F>
auto write(F&& f)->std::result_of_t<F(T&)> {
return access(std::forward<F>(f), *this);
}
// uses `const` ness of Syncs to determine access:
template<class F, class... Syncs>
friend auto access( F&& f, Syncs&&... syncs )->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... );
};
synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){}
synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){}
// special member functions:
synchronized( T & o ):t(o) {}
synchronized( T const& o ):t(o) {}
synchronized( T && o ):t(std::move(o)) {}
synchronized( T const&& o ):t(std::move(o)) {}
synchronized& operator=(T const& o) {
write([&](T& t){
t=o;
});
return *this;
}
synchronized& operator=(T && o) {
write([&](T& t){
t=std::move(o);
});
return *this;
}
private:
template<class X, class S>
static auto smart_lock(S const& s) {
return std::shared_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class X, class S>
static auto smart_lock(S& s) {
return std::unique_lock< std::shared_timed_mutex >(s.m, X{});
}
template<class L>
static void lock(L& lockable) {
lockable.lock();
}
template<class...Ls>
static void lock(Ls&... lockable) {
std::lock( lockable... );
}
template<size_t...Is, class F, class...Syncs>
friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)->
std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
{
auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... );
lock( std::get<Is>(locks)... );
return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...);
}
mutable std::shared_timed_mutex m;
T t;
};
template<class T>
synchronized< T > sync( T&& t ) {
return {std::forward<T>(t)};
}
Funktionen für C ++ 14 und C ++ 1z enthalten.
Dies setzt voraus, dass const
Vorgänge für mehrere Leser sicher sind (was std
Container voraussetzen).
Verwendung sieht aus wie:
synchronized<int> x = 7;
x.read([&](auto&& v){
std::cout << v << '\n';
});
für einen int
mit synchronisiertem Zugriff.
Ich würde davon abraten synchronized(synchronized const&)
. Es wird selten benötigt.
Wenn Sie benötigen synchronized(synchronized const&)
, würde ich zu ersetzen versucht sein , T t;
mit std::aligned_storage
, so dass manuelle Platzierung Konstruktion und manuelle Zerstörung tun. Dies ermöglicht eine ordnungsgemäße Lebensdauerverwaltung.
Ansonsten könnten wir die Quelle kopieren T
und dann daraus lesen:
synchronized(synchronized const& o):
t(o.read(
[](T const&o){return o;})
)
{}
synchronized(synchronized && o):
t(std::move(o).read(
[](T&&o){return std::move(o);})
)
{}
zur Zuordnung:
synchronized& operator=(synchronized const& o) {
access([](T& lhs, T const& rhs){
lhs = rhs;
}, *this, o);
return *this;
}
synchronized& operator=(synchronized && o) {
access([](T& lhs, T&& rhs){
lhs = std::move(rhs);
}, *this, std::move(o));
return *this;
}
friend void swap(synchronized& lhs, synchronized& rhs) {
access([](T& lhs, T& rhs){
using std::swap;
swap(lhs, rhs);
}, *this, o);
}
Die Platzierung und die ausgerichteten Speicherversionen sind etwas unordentlicher. Der meiste Zugriff auf t
würde durch eine Mitgliedsfunktion ersetzt T&t()
und T const&t()const
, außer bei Bauarbeiten, bei denen Sie durch einige Reifen springen müssten.
Indem wir synchronized
einen Wrapper anstelle eines Teils der Klasse erstellen, müssen wir nur sicherstellen, dass die Klasse intern const
als Mehrfachleser gilt, und ihn in einem Thread schreiben.
In den seltenen Fällen, in denen wir eine synchronisierte Instanz benötigen, springen wir durch Reifen wie oben.
Entschuldigung für Tippfehler oben. Es gibt wahrscheinlich einige.
Ein Nebeneffekt des oben Gesagten besteht darin, dass n-ary beliebige Operationen an synchronized
Objekten (desselben Typs) zusammenarbeiten, ohne dass sie vorher hart codiert werden müssen. Fügen Sie eine Freunddeklaration hinzu, und n-ary synchronized
Objekte mehrerer Typen können zusammenarbeiten. access
In diesem Fall muss ich möglicherweise nicht mehr ein Inline-Freund sein, um mit Überlastungskonflikten fertig zu werden.
Die Verwendung von Mutexen und C ++ - Verschiebungssemantik ist eine hervorragende Möglichkeit, Daten sicher und effizient zwischen Threads zu übertragen.
Stellen Sie sich einen "Produzenten" -Thread vor, der Stapel von Zeichenfolgen erstellt und diese (einem oder mehreren) Verbrauchern zur Verfügung stellt. Diese Stapel könnten durch ein Objekt dargestellt werden, das (möglicherweise große) std::vector<std::string>
Objekte enthält. Wir möchten unbedingt den internen Zustand dieser Vektoren ohne unnötige Doppelarbeit in ihre Verbraucher verschieben.
Sie erkennen den Mutex einfach als Teil des Objekts und nicht als Teil des Objektzustands. Das heißt, Sie möchten den Mutex nicht verschieben.
Welche Sperre Sie benötigen, hängt von Ihrem Algorithmus ab oder davon, wie allgemein Ihre Objekte sind und welchen Verwendungsbereich Sie zulassen.
Wenn Sie immer nur von einem "Produzenten" -Objekt mit gemeinsamem Status zu einem threadlokalen "konsumierenden" Objekt wechseln, können Sie möglicherweise nur das vom Objekt verschobene Objekt sperren .
Wenn es sich um ein allgemeineres Design handelt, müssen Sie beide sperren. In einem solchen Fall müssen Sie dann ein Deadlocking in Betracht ziehen.
Wenn dies ein potenzielles Problem ist, verwenden Sie std::lock()
diese Option, um Sperren für beide Mutexe auf Deadlock-freie Weise zu erwerben.
http://en.cppreference.com/w/cpp/thread/lock
Abschließend müssen Sie sicherstellen, dass Sie die Verschiebungssemantik verstehen. Denken Sie daran, dass das vom Objekt verschobene Objekt in einem gültigen, aber unbekannten Zustand belassen wird. Es ist durchaus möglich, dass ein Thread, der die Verschiebung nicht ausführt, einen gültigen Grund hat, auf das verschobene Objekt zuzugreifen, wenn er diesen gültigen, aber unbekannten Status findet.
Wieder schlägt mein Produzent nur Saiten aus und der Verbraucher nimmt die ganze Ladung weg. In diesem Fall kann der Produzent jedes Mal, wenn er versucht, dem Vektor etwas hinzuzufügen, feststellen, dass der Vektor nicht leer oder leer ist.
Kurz gesagt, wenn der potenzielle gleichzeitige Zugriff auf das vom Objekt verschobene Objekt einen Schreibvorgang darstellt, ist dies wahrscheinlich in Ordnung. Wenn es sich um einen Lesevorgang handelt, überlegen Sie, warum es in Ordnung ist, einen beliebigen Status zu lesen.
Zunächst muss etwas mit Ihrem Design nicht stimmen, wenn Sie ein Objekt mit einem Mutex verschieben möchten.
Wenn Sie sich dennoch dazu entschließen, müssen Sie im Verschiebungskonstruktor einen neuen Mutex erstellen, z. B.:
// movable
struct B{};
class A {
B b;
std::mutex m;
public:
A(A&& a)
: b(std::move(a.b))
// m is default-initialized.
{
}
};
Dies ist threadsicher, da der Verschiebungskonstruktor davon ausgehen kann, dass sein Argument nirgendwo anders verwendet wird, sodass das Sperren des Arguments nicht erforderlich ist.
a.mutex
gesperrt ist?: Sie verlieren diesen Zustand. -1
A a; A a2(std::move(a)); do some stuff with a
.
new
, die Instanz hochzufahren und in eine zu platzieren std::unique_ptr
, was sauberer erscheint und wahrscheinlich nicht zu Verwirrungsproblemen führt. Gute Frage.