I've recently written a Vulkan library for creating 2D applications with ease. The catch was I need std::lock_guard
for my window resize event to resize resources related to the window on a secondary render thread (separate from the main GLFW window thread).
While testing on an AMD machine (running on an AMD APU 5600G) std::lock_guard
would crash my program. I thought it was strange and tried it again using a blank console program and the same issue happened. Presumably due to issues with memory ordering with caching hits when attempting to lock mutexes on (some?) AMD platforms. The solution was to use std::atomic
to force standardized memory ordering.
The end result was my own custom atomic_lock
and atomic_mutex
. This should work similarly to std::lock_guard
except with an escape timeout (in milliseconds):
#pragma once
#ifndef ATOMIC_LOCK
#define ATOMIC_LOCK
#include <mutex>
struct atomic_mutex {
public:
std::atomic_bool signal;
std::mutex lock;
};
template<bool wait = false, size_t timeout = 100>
class _NODISCARD_LOCK atomic_lock {
private:
std::atomic_bool signal;
public:
atomic_mutex& lock;
~atomic_lock() noexcept { ForceUnlock(); }
atomic_lock(const atomic_lock&) = delete;
atomic_lock& operator=(const atomic_lock&) = delete;
explicit atomic_lock(atomic_mutex& lock) : lock(lock) {
signal = static_cast<bool>(lock.signal);
if constexpr(wait) {
std::chrono::time_point<std::chrono::system_clock> now = std::chrono::system_clock::now();
std::chrono::milliseconds startTime = duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
std::chrono::milliseconds endTime = duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
while (signal) {
endTime = duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
if (endTime.count() - startTime.count() >= static_cast<long long>(timeout))
break;
signal = static_cast<bool>(lock.signal);
}
}
lock.signal = !signal;
if (signal == false)
lock.lock.lock();
}
bool AcquiredLock() { return !signal; }
void ForceUnlock() {
if (!signal) {
signal = true;
lock.signal = false;
lock.lock.unlock();
}
}
};
#endif
atomic_lock
takes in an atomic_mutex
(struct with atomic_bool & mutex, see above) and attempts to acquire the mutex before the timeout. Then provides a function with the acquired lock state and another to unlock if needed. Then will unlock itself on destruct (out-of-scope).
Usage Pattern:
atomic_mutex mylock; // default constructors for std::atomic_bool and std::mutex.
void function(atomic_mutex& mutex) {
atomic_lock alock(mutex);
// atomic_lock<true, 500> alock(mutex) // template params optional.
// Do some multi-threaded work...
}
std::lock_guard
on Stack Overflow? Perhaps you made a mistake, or perhaps there's a serious problem with this particular implementation of the standard library. \$\endgroup\$std::lock_guard
. \$\endgroup\$