Централизованный справедливый спин-замок RW - PullRequest
0 голосов
/ 16 мая 2019

В статье 1991 года «Масштабируемая синхронизация устройства чтения-записи для мультипроцессоров с общей памятью» Джона М. Меллора-Крамми и Майкла Л. Скотта имеется справедливая блокировка RW на основе алгоритма блокировки билета (https://www.cs.rochester.edu/u/scott/papers/1991_PPoPP_read_write.pdf).

В псевдокоде это выглядит так (http://www.cs.rochester.edu/research/synchronization/pseudocode/rw.html):

type counter = unsigned integer
  // layout of counter
  //  31    ...   16 15    ...     0        
  // +------------------------------+
  // | reader count | writer count  |
  // +------------------------------+

const RC_INCR = 0x10000 // to adjust reader count
const WC_INCR = 0x1     // to adjust writer count
const W_MASK  = 0xffff  // to extract writer count

// mask bit for top of each count
const WC_TOPMSK = 0x8000      
const RC_TOPMSK = 0x80000000 

type lock = record
    requests : counter := 0
    completions : counter := 0

procedure start_write (L : ^lock)
    counter prev_processes := 
        fetch_and_clear_then_add (&L->requests, WC_TOPMSK, WC_INCR)
    repeat until completions = prev_processes

procedure start_read (L : ^lock)
    counter prev_writers := 
        fetch_and_clear_then_add (&L->requests, RC_TOPMSK, RC_INCR) & W_MASK
    repeat until (completions & W_MASK) = prev_writers

procedure end_write (L: ^lock)
    clear_then_add (&L->completions, WC_TOPMSK, WC_INCR)

procedure end_read (L : ^lock)
    clear_then_add (&L->completions, RC_TOPMSK, RC_INCR)

Используются примитивы fetch_and_clear_then_add и clear_then_add, которые не реализованы в современном оборудовании. Очевидно, что их можно эмулировать с помощью программного CAS-цикла.

Однако мне пришло в голову, что обоснование использования этих примитивов (предотвращение переполнения) исходит из чистой интуиции, чтобы держать счетчики читателей и писателей раздельными и точными. Но, как это бывает, интуиция может быть неправильной. Я считаю, логика предотвращения переполненияможно удалить без какого-либо вреда для алгоритма подчиненного.

Я пытался найти похожие результаты в Google, но пока ничего не нашел. Возможно, я что-то упускаю.

Итак, позвольте мне объяснить мою идею вболее подробно. Я думаю, что стандартная инструкция fetch_add будет работатьпросто отлично с этим алгоритмом.

  1. Количество читателей содержится в верхней половине слова.Если fetch_add (0x10000) приводит к переполнению, лишний бит будет молча выпадать из границы слова.Здесь нет проблем.

  2. Количество писателей содержится в нижней половине мира.Если fetch_add (1) приводит к переполнению, дополнительный бит будет перетекать в счетчик считывателей.К сожалению.Один из 2 ^ 16 авторов нарушает наши правила, это портит счет читателя.Но подождите, это действительно проблема?Что у нас после этого?Посмотрим.В слове requests мы имеем следующий переход:

    (0xYYYY, 0xFFFF) -> (0xYYYY + 1, 0x0000) // 0xYYYY обозначает неизвестный счетчик чтения, 0xFFFF представляет счетчик записи перед переполнением

В то же время слово completions содержит значение, меньшее или равное ( 0xYYYY, 0xFFFF ).И поток 'нарушителя' начинает опрашивать слово completions, пока оно не станет точно равным требуемому значению.Любые читатели или писатели, которые начинали раньше, не могут создать какое-либо запутанное значение в слове completions.По мере того, как они прогрессируют и делают разблокировку, они неизменно достигнут требуемой величины.После этого запускается поток «нарушителя».

С другой стороны, любой новый поток, который появится позже, также не увидит никакого сбивающего с толку значения.Писатели увеличивают нижнюю половину слова, читатели увеличивают верхнюю половину.Они остаются заказанными друг против друга.И не может продолжаться до тех пор, пока поток «нарушителя» не выполнит свою работу и не разблокируется.

Разблокировка, выполненная «нарушителем», полностью компенсирует его нарушение.Это приведет к симметричному переполнению счетчика писателей в слове completions и добавит 1 к счетчику читателей.Таким образом, следующий поступивший поток начнется точно по расписанию, так как ожидал именно этого «просчитанного» значения.Все это будет работать без сучка.

Таким образом, общий результат состоит в том, что при 2 ^ 16 блокировках записи число блокировок чтения становится искаженным на 1. Все остальное выглядит хорошо.

ЛегкоНеофициальной иллюстрацией этому является ситуация, когда вообще нет взломанных считывателей.В этом случае RW-блокировка со стандартными инструкциями fetch_add () будет вести себя как обычная блокировка билета.Количество читателей будет постоянно увеличиваться на одну каждые 2 ^ 16 блокировок писателя.Но это различие мы делаем в нашем уме.На уровне машины это всего лишь шаг в 2 ^ 32 слова.На данный момент нет разницы с блокировкой простого билета.

Теперь читатели добавлены к картинке.Я не могу придумать никакого графика, когда прибывающие читатели, увеличивая только верхнюю половину слова, могут испортить всю картину.С точки зрения писателя прибытие читателя выглядит как внезапное прибытие 2 ^ 16 писателей одновременно.С этой точки зрения он по-прежнему работает как обычная блокировка билетов.

Теперь с точки зрения читателя все по-другому.Они выглядяттолько по количеству писателей и вообще не заботятся о любых переполнениях в верхнюю половину слова.Когда читатель прибывает, он вспоминает о последнем прибывшем писателе и отгоняет любых будущих писателей этим огромным шагом 2 ^ 16.Затем он ждет, пока запомнившийся писатель закончит и продолжит свою собственную работу.Когда это сделано, он делает еще один огромный шаг 2 ^ 16, так что любой, возможно, ожидающий ничего не подозревающий и наивный писатель может прийти к выводу, что это целая куча из 2 ^ 16 одновременно работающих писателей.

Пожалуйста, исправьте меня, если янеправильно с моими рассуждениями.

Реализация этого на C ++ выглядит следующим образом:

class shared_ticket_lock : non_copyable
{
public:
    void lock() noexcept
    {
        base_type tail = tail_.fetch_add(exclusive_step, std::memory_order_relaxed);
        for (;;) {
            base_type head = head_.load(std::memory_order_acquire);
            if (tail == head)
                break;
        }
    }

    void unlock() noexcept
    {
        base_type head = head_.load(std::memory_order_relaxed);
        head_.store(head + exclusive_step, std::memory_order_release);
    }

    void lock_shared() noexcept
    {
        base_type tail = tail_.fetch_add(shared_step, std::memory_order_relaxed);
        for (tail &= exclusive_mask;;) {
            base_type head = head_.load(std::memory_order_acquire);
            if (tail == (head & exclusive_mask))
                break;
        }
    }

    void unlock_shared() noexcept
    {
        head_.fetch_add(shared_step, std::memory_order_release);
    }

private:
    using base_type = std::uint32_t;
    static constexpr base_type shared_step = 1 << 16;
    static constexpr base_type exclusive_mask = shared_step - 1;
    static constexpr base_type exclusive_step = 1;

    std::atomic<base_type> head_ = ATOMIC_VAR_INIT(0);
    std::atomic<base_type> tail_ = ATOMIC_VAR_INIT(0);
};

ПРИМЕЧАНИЕ: в оригинальной статье отмечается, что можно реализовать справедливую блокировку RW с обычным fetch_add, нокод будет немного сложнее.Непонятно, что имели в виду авторы.Я предполагаю что-то другое (например, поддержание счетчиков чтения и записи в отдельных словах), потому что, насколько я вижу, это абсолютно НЕ сложнее.

ОБНОВЛЕНИЕ: Рабочий код и простой тест можно найти здесь:

...