Подтвердить что ты не робот

Реализация С++ shared_mutex

boost::shared_mutex или std::shared_mutex (С++ 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
    {
        // This must be a release operation (see answer)
        refcount.fetch_sub(1, std::memory_order_relaxed);
    }
};
4b9b3361

Ответ 1

(CAS = Compare And Swap = C++ compare_exchange_weak функция, которая в x86 обычно компилируется в инструкцию lock cmpxchg, которая может выполняться только в том случае, если ей принадлежит строка кэша в Exclusive или Modified. MESI государство).


lock_shared выглядит хорошо: вращение только для чтения с попыткой CAS только тогда, когда это кажется возможным, лучше для производительности, чем вращение на CAS или атомное приращение. Вы уже должны были выполнить проверку только для чтения, чтобы избежать изменения -1 на 0 и разблокировки блокировки записи.

На платформе x86 поместите _mm_pause() в путь повторения цикла вращения, чтобы избежать ядерных ошибок конвейера неправильной спекуляции порядка памяти при выходе из цикла вращения только для чтения, и во время вращения похищайте меньше ресурсов из другой гиперпотоки. (Используйте цикл while(), а не do{}while(), поэтому пауза запускается только после однократного сбоя. pause на Skylake и позже ждет около 100 циклов, поэтому избегайте этого в быстром пути.)


Я думаю, что unlock_shared должен использовать mo_release, а не mo_relaxed, так как ему необходимо упорядочить загрузки из общей структуры данных, чтобы убедиться, что записывающее устройство не начинает запись до того, как произойдет загрузка из критического раздела читателя., (Переупорядочение в LoadStore характерно для слабо упорядоченных архитектур, хотя x86 выполняет переупорядочение только в StoreLoad.) Операция Release упорядочивает предшествующие загрузки и удерживает их в критической секции.


(в записи lock)://можно ли использовать memory_order_relaxed, если только один поток блокирует запись?

Нет, вам все еще нужно хранить записи в критической секции, поэтому CAS все еще нужно синхронизировать с (в терминологии C++) хранилищами релизов из unlock_shared.

https://preshing.com/20120913/acquire-and-release-semantics/  имеет хорошее изображение, которое показывает односторонний барьерный эффект релиз-магазина или приобретения-загрузки.