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;
};
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();
}
};
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);
}
};
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?
Mobile version