Ist es möglich, einen Mutex aus C++20 std::atomic zu erstellen, ohne ihn zu drehen, um die Verwendung nach dem FreigebenC++

Programme in C++. Entwicklerforum
Anonymous
 Ist es möglich, einen Mutex aus C++20 std::atomic zu erstellen, ohne ihn zu drehen, um die Verwendung nach dem Freigeben

Post by Anonymous »

Bevor ich zu meiner Hauptfrage komme, gehe ich davon aus, dass std::mutex::unlock() dies nicht mehr berührt, sobald der Mutex in einen Zustand versetzt wird, in dem ein anderer Thread ihn möglicherweise sperrt – auch wenn der Aufruf von unlock() möglicherweise noch nicht zurückgekehrt ist. Mit anderen Worten, der folgende C++-Code ist korrekt:

Code: Select all

#include 
#include 
#include 

struct RefCount {
std::mutex m_;
std::size_t c_{};

// Always construct on heap
static RefCount *make() { return new RefCount; }
virtual ~RefCount() = default;

void incref()
{
m_.lock();
++c_;
m_.unlock();
}

void decref()
{
m_.lock();
auto c = --c_;
m_.unlock();
if (!c)
delete this;
}

protected:
RefCount() noexcept = default;
};
Insbesondere sollte es kein undefiniertes Verhalten geben, wenn zwei Threads den Referenzzähler halten und decref() aufrufen – der erste Thread entsperrt den Mutex funktional, obwohl unlock() noch nicht zurückgekehrt ist, dann entsperrt der zweite Thread den Mutex und zerstört das Objekt einschließlich des Mutex, und erst dann kehrt der erste Thread vom Aufruf von unlock() zurück.
Beispiele wie die oben genannten würden dies leider tun scheinen einfache Futex-basierte Mutexe auszuschließen, die mit std::atomic implementiert werden. Das übliche Muster wäre, drei Zustände zu haben (entsperrt, gesperrt und mit Kellnern gesperrt), die wie folgt aussehen könnten:

Code: Select all

struct BadMutex {
static constexpr uint8_t kUnlocked = 0;
static constexpr uint8_t kLocked = 1;
static constexpr uint8_t kLockedWantNotify = 2;
std::atomic_uint8_t state_{kUnlocked};

void lock()
{
for (;;) {
auto s = state_.exchange(kLocked, std::memory_order_acquire);
if (s != kUnlocked)
s = state_.exchange(kLockedWantNotify, std::memory_order_acquire);
if (s == kUnlocked)
return;
state_.wait(kLockedWantNotify);
}
}

void unlock()
{
if (state_.exchange(kUnlocked, std::memory_order_release) == kLockedWantNotify)
// UB if BadMutex destroyed here
state_.notify_all();
}
};
Wenn Sie BadMutex oben in RefCount einbinden, kann es leider zu undefiniertem Verhalten kommen, wenn der vorletzte decref()-Thread direkt vor state_.notify_all() unterbrochen wird und BadMutex zerstört wird, da das Aufrufen einer Methode für ein zerstörtes Objekt UB ist.
Beachten Sie, dass die Verwendung eines raw Der Systemaufruf FUTEX_WAKE wäre in Ordnung, da es keine große Sache ist, eine falsche Weckfunktion zu senden oder sogar FUTEX_WAKE für nicht zugeordneten Speicher aufzurufen (holen Sie sich einfach ein EFAULT, das Sie ignorieren können). In der Praxis sollte dieses UB also keine große Sache sein, aber natürlich steht die Geschichte einem absichtlichen UB nicht wohlwollend gegenüber.
Meine nächste Frage lautet also: Kann ich etwas dagegen tun? Das Beste, was ich mir ausgedacht habe, ist, sich während der Zerstörung zu drehen, aber es ist anstößig, sich drehen zu müssen, wenn wir Futexes haben, die speziell darauf ausgelegt sind, Drehungen zu vermeiden. Das Beste, was mir einfällt, ist so etwas:

Code: Select all

struct UglyMutex {
static constexpr uint8_t kUnlocked = 0;
static constexpr uint8_t kLocked = 1;
static constexpr uint8_t kLockedWantNotify = 2;
std::atomic_uint8_t state_{kUnlocked};
std::atomic_uint16_t unlocking_{0};

~UglyMutex()
{
while (unlocking_.load(std::memory_order_acquire))
;
}

void lock()
{
for (;;) {
auto s = state_.exchange(kLocked, std::memory_order_acquire);
if (s != kUnlocked)
s = state_.exchange(kLockedWantNotify, std::memory_order_acquire);
if (s == kUnlocked)
return;
state_.wait(kLockedWantNotify);
}
}

void unlock()
{
unlocking_.fetch_add(1, std::memory_order_relaxed);
if (state_.exchange(kUnlocked, std::memory_order_release) == kLockedWantNotify)
state_.notify_all();
unlocking_.fetch_sub(1, std::memory_order_release);
}
};
Übersehe ich einen anderen Trick, den ich verwenden könnte, um dies ohne Spinning zu tun – vielleicht mit std::atomic_ref?
Gibt es Hoffnung, dass eine zukünftige Version von C++ eine sichere Möglichkeit bietet, über zerstörte Atomics zu benachrichtigen?
Gibt es einen Vorteil für die Sprache, die dieses UB erstellt, oder handelt es sich im Grunde genommen um einen Fehler in der Sprachspezifikation, dass Atomics nicht die volle Ausdruckskraft von Atomics offenlegen die zugrunde liegenden Futexe, auf denen sie implementiert sind?

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post