2015-10-06 2 views
5

Betrachten Sie das folgende Beispiel.Warum gibt es keine Wartefunktion für condition_variable, die den Mutex nicht verriegelt

std::mutex mtx; 
std::condition_variable cv; 

void f() 
{ 
    { 
    std::unique_lock<std::mutex> lock(mtx); 
    cv.wait(lock); // 1 
    } 
    std::cout << "f()\n"; 
} 

void g() 
{ 
    std::this_thread::sleep_for(1s); 
    cv.notify_one(); 
} 

int main() 
{ 
    std::thread t1{ f }; 
    std::thread t2{ g }; 
    t2.join(); 
    t1.join(); 
} 

g() „weiß“, dass f() im Szenario wartet möchte ich besprechen möchte. Gemäß cppreference.com ist es nicht erforderlich, g() den Mutex vor dem Aufruf notify_one zu sperren. In der mit "1" gekennzeichneten Zeile wird cv den Mutex freigeben und erneut sperren, sobald die Benachrichtigung gesendet wurde. Der Destruktor von lock gibt es sofort danach wieder frei. Dies scheint überflüssig zu sein, insbesondere da das Sperren teuer ist. (Ich weiß in bestimmten Szenarien muss der Mutex gesperrt sein. Aber das ist hier nicht der Fall.)

Warum hat condition_variable keine Funktion "wait_nolock", die den Mutex nicht erneut sperrt, sobald die Benachrichtigung eintrifft. Wenn die Antwort lautet, dass Pthreads keine solche Funktionalität bieten: Warum können Pthreads nicht erweitert werden, um sie bereitzustellen? Gibt es eine Alternative, um das gewünschte Verhalten zu realisieren?

+0

Warum denken Sie, dass dies etwas mit Pthreads zu tun hat? –

+7

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. –

+1

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

Antwort

13

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.

+0

Ich denke, Ihr Argument ist korrekt, es sei denn, die Variable, die unerwünschte Wakeups erkennt, ist atomar. Dies könnte der Fall sein, wenn direkt nach dem Warten eine Schleife verarbeitet wird und die obere Grenze die atomare Variable ist. Im Falle eines ungewollten Aufweckens wird diese Schleife nicht verarbeitet und alles ist in Ordnung. –

+0

"Dann (wenn der Mutex möglicherweise deaktiviert ist) wird die condition_variable benachrichtigt" würde dies nicht zu einer Wettlaufsituation führen? – Slava

+0

@Slava Im nicht-formalen Sinn, ja. In dem durch den C++ - Standard definierten Sinn, nein (eine C++ - Standard-Race-Bedingung bewirkt UB: das Aufrufen des Signals ohne den Mutex verursacht kein UB, kann aber dazu führen, dass Signale verloren gehen, was eines der Dinge ist, die nicht C++ sind Standard "Race Condition" Begriff kann beschreiben). – Yakk

Verwandte Themen