Sie missverstehen, was Ihr Code tut.
Ihr Code auf Linie // 1
ist frei, überhaupt nicht zu blockieren. condition_variables
können (und werden!) Falsche Wakeups haben - sie können ohne guten Grund aufwachen.
Sie sind dafür verantwortlich, zu überprüfen, ob das Aufwecken falsch ist.
ein condition_variable
Verwendung erfordert richtig 3 Dinge:
- A
condition_variable
- A
mutex
- Einige von den
mutex
bewacht Daten
Der von der Mutex bewachten Daten modifiziert werden (unter die mutex
). Dann (mit der mutex
möglicherweise deaktiviert) wird die condition_variable
benachrichtigt.
Auf der anderen Seite, sperren Sie die mutex
, dann warten auf die Bedingung Variable. Wenn Sie aufwachen, wird Ihre mutex
wieder gesperrt, und Sie testen, ob das Wecken falsch ist, indem Sie die Daten betrachten, die von mutex
geschützt werden. Wenn es sich um ein gültiges Wakeup handelt, verarbeiten Sie und fahren fort.
Wenn es kein gültiges Wakeup war, gehen Sie zurück zum Warten.
In Ihrem Fall haben Sie keine Daten bewacht, Sie können nicht weckende Wakeups von echten unterscheiden, und Ihr Design ist unvollständig.
Nicht überraschend mit dem unvollständigen Design sehen Sie nicht den Grund, warum die mutex
wieder gesperrt ist: Es ist wieder verschlossen, so dass Sie die Daten sicher überprüfen können, ob das Wecken falsch war oder nicht.
Wenn Sie wissen möchten, warum Bedingungsvariablen auf diese Weise entworfen sind, wahrscheinlich weil dieses Design effizienter als das "zuverlässige" ist (aus welchen Gründen auch immer), anstatt höhere Ebenen-Primitive offenzulegen, enthüllte C++ die untere Ebene mehr effiziente Primitive.
Der Aufbau einer Abstraktion auf höherer Ebene ist nicht schwer, aber es gibt Designentscheidungen. Hier ist eine auf der std::experimental::optional
gebaut:
template<class T>
struct data_passer {
std::experimental::optional<T> data;
bool abort_flag = false;
std::mutex guard;
std::condition_variable signal;
void send(T t) {
{
std::unique_lock<std::mutex> _(guard);
data = std::move(t);
}
signal.notify_one();
}
void abort() {
{
std::unique_lock<std::mutex> _(guard);
abort_flag = true;
}
signal.notify_all();
}
std::experimental::optional<T> get() {
std::unique_lock<std::mutex> _(guard);
signal.wait(_, [this]()->bool{
return data || abort_flag;
});
if (abort_flag) return {};
T retval = std::move(*data);
data = {};
return retval;
}
};
nun jeder send
ein get
am anderen Ende zum Erfolg führen kann. Wenn mehr als eine send
auftritt, wird nur die letzte von einer get
verbraucht. Wenn und wenn abort_flag
gesetzt ist, gibt get()
sofort {}
zurück;
Das oben genannte unterstützt mehrere Verbraucher und Hersteller.
Ein Beispiel für die Verwendung des oben genannten Beispiels ist eine Quelle für den Vorschaustatus (z. B. einen UI-Thread) und einen oder mehrere Vorschau-Renderer (die nicht schnell genug sind, um im UI-Thread ausgeführt zu werden).
Der Vorschau-Status gibt einen Vorschau-Status in die willy-nilly. Die Renderer konkurrieren und einer von ihnen ergreift es. Dann rendern sie es und geben es zurück (durch welchen Mechanismus auch immer).
Wenn die Vorschau-Zustände schneller kommen, als die Renderer sie konsumieren, ist nur der letzte von Interesse, daher werden die früheren verworfen. Vorhandene Vorschauen werden jedoch nicht abgebrochen, nur weil ein neuer Status angezeigt wird.
Fragen, die unten zu den Rennbedingungen gestellt wurden.
Wenn die Daten, die kommuniziert werden, atomic
sind, können wir nicht ohne den Mutex auf der "Sende" -Seite auskommen?
So etwas wie folgt aus:
template<class T>
struct data_passer {
std::atomic<std::experimental::optional<T>> data;
std::atomic<bool> abort_flag = false;
std::mutex guard;
std::condition_variable signal;
void send(T t) {
data = std::move(t); // 1a
signal.notify_one(); // 1b
}
void abort() {
abort_flag = true; // 1a
signal.notify_all(); // 1b
}
std::experimental::optional<T> get() {
std::unique_lock<std::mutex> _(guard); // 2a
signal.wait(_, [this]()->bool{ // 2b
return data.load() || abort_flag.load(); // 2c
});
if (abort_flag.load()) return {};
T retval = std::move(*data.load());
// data = std::experimental::nullopt; // doesn't make sense
return retval;
}
};
die oben nicht funktioniert.
Wir beginnen mit dem hörenden Thread. Es tut Schritt 2a, dann wartet (2b). Er bewertet die Bedingung in Schritt 2c, kehrt aber nicht vom Lambda zurück.
Der Broadcasting-Thread führt dann Schritt 1a aus (Einstellung der Daten) und signalisiert dann die Bedingungsvariable. In diesem Moment wartet niemand auf die Bedingungsvariable (der Code im Lambda zählt nicht!).
Der hörende Thread beendet dann das Lambda und gibt "spurious wakeup" zurück. Es blockiert dann die Bedingungsvariable und bemerkt nie, dass Daten gesendet wurden.
Die std::mutex
verwendet, während auf der Bedingungsvariable warten, um die Schreiboperation auf die Daten schützen müssen „Bestanden“ durch die Zustandsgröße (was auch immer Test, den Sie bestimmen tun, wenn die Wake-up-unechten war), und die Lese (im Lambda), oder die Möglichkeit von "verlorenen Signalen" besteht. (Zumindest in einer einfachen Implementierung: komplexere Implementierungen können blockfreie Pfade für "häufige Fälle" erstellen und nur die mutex
in einer doppelten Überprüfung verwenden. Dies würde den Rahmen dieser Frage sprengen.)
Verwenden atomic
Variablen Umgeht dieses Problem nicht, weil die beiden Operationen "feststellen, ob die Nachricht falsch war" und "in der Bedingungsvariablen aufwarten" in Bezug auf die "Unechtheit" der Nachricht atomar sein müssen.
Warum denken Sie, dass dies etwas mit Pthreads zu tun hat? –
Sie können 'condition_variable' auf diese Weise nicht zuverlässig verwenden, da es zu unerwünschten Wake-ups kommen kann. Eine 'condition_variable' schützt normalerweise einen Zustand und wird verwendet, um zu warten, bis ein Prädikat über diesen Zustand wahr wird. Wenn es aufwacht, überprüfen Sie das Prädikat und gehen Sie in den Schlafmodus, wenn es falsch ist. Aber der Zugriff auf den Shared-State muss natürlich unter einem Mutex erfolgen. –
Wie üblich verwechseln Sie Zustandsvariablen mit Signalen. POSIX hat keine Signale, und während Zustandsvariablen dazu verwendet werden können, sie nachzuahmen, sind sie keine Signale an sich. – SergeyA