C++ shared_mutex реализация
boost::shared_mutex
или же std::shared_mutex
(C++17) может быть использован для одного писателя, несколько читателей доступа. В качестве учебного упражнения я собрал простую реализацию, которая использует спин-блокировку и имеет другие ограничения (например, политику справедливости), но, очевидно, не предназначена для использования в реальных приложениях.
Идея состоит в том, что мьютекс сохраняет счетчик ссылок равным нулю, если ни один поток не удерживает блокировку. Если> 0, значение представляет количество читателей, которые имеют доступ. Если -1, один автор имеет доступ.
Является ли это правильной реализацией (в частности, с использованным, минимальным, упорядочением памяти), в которой нет гонок данных?
#include <atomic>
class my_shared_mutex {
std::atomic<int> refcount{0};
public:
void lock() // write lock
{
int val;
do {
val = 0; // Can only take a write lock when refcount == 0
} while (!refcount.compare_exchange_weak(val, -1, std::memory_order_acquire));
// can memory_order_relaxed be used if only a single thread takes write locks ?
}
void unlock() // write unlock
{
refcount.store(0, std::memory_order_release);
}
void lock_shared() // read lock
{
int val;
do {
do {
val = refcount.load(std::memory_order_relaxed);
} while (val == -1); // spinning until the write lock is released
} while (!refcount.compare_exchange_weak(val, val+1, std::memory_order_acquire));
}
void unlock_shared() // read unlock
{
refcount.fetch_sub(1, std::memory_order_relaxed);
}
};
1 ответ
(Я использую cmpxchg как сокращение для C++ compare_exchange_weak
функция, а не инструкция x86 cmpxchg).
lock_shared
определенно выглядит хорошо: вращается при чтении и пытается cmpxchg только тогда, когда значение намного лучше для производительности, чем вращение при cmpxchg. Хотя я думаю, что вы были вынуждены сделать это для правильности, чтобы избежать изменения -1 на 0 и разблокировки блокировки записи.
Я думаю unlock_shared
следует использовать mo_release
не mo_relaxed
, поскольку необходимо упорядочить загрузки из общей структуры данных, чтобы убедиться, что записывающее устройство не начинает запись до того, как произойдет загрузка из критического раздела считывателя. ( Переупорядочение LoadStore характерно для слабо упорядоченных архитектур, хотя x86 выполняет только переупорядочивание StoreLoad.) Операция Release упорядочивает предшествующие загрузки и удерживает их внутри критической секции.
(в записи
lock
): // можно ли использовать memory_order_relaxed, если только один поток блокирует запись?
Нет, вам все еще нужно хранить записи внутри критической секции, поэтому cmpxchg все еще нужно синхронизировать с (в терминологии C++) выпуски хранилища из unlock_shared
,