Быстрая и блокировка Free Single Writer, несколько читателей - PullRequest
0 голосов
/ 10 января 2019

У меня есть один писатель, который должен увеличивать переменную с довольно высокой частотой, а также один или несколько читателей, которые обращаются к этой переменной с более низкой частотой.

Запись инициируется внешним прерыванием.

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

Подход, который я придумал, копировал значение после записи в него. Читатель теперь может сравнить оригинал с копией. Если они равны, содержимое переменной допустимо.

Вот моя реализация на C ++

template<typename T>
class SafeValue
{
private:
    volatile T _value;
    volatile T _valueCheck;
public:
    void setValue(T newValue)
    {
        _value = newValue;
        _valueCheck = _value;
    }

    T getValue()
    {
        volatile T value;
        volatile T valueCheck;
        do
        {
            valueCheck = _valueCheck;
            value = _value;
        } while(value != valueCheck);

        return value;
    }
}

Идея, стоящая за этим, заключается в обнаружении гонок данных во время чтения и повторной попытки, если они произойдут. Тем не менее, я не знаю, будет ли это всегда работать. Я не нашел ничего об этом доступе онлайн, поэтому мой вопрос:

Есть ли проблемы с моим подходом при использовании с одним писателем и несколькими читателями?

Я уже знаю, что высокая частота записи может вызвать голод у читателя. Есть ли еще плохие последствия, о которых я должен быть осторожен? Может ли быть так, что это вообще не потокобезопасно?

Редактировать 1:

Моя целевая система - ARM Cortex-A15.

T должен иметь возможность стать хотя бы любым примитивным целочисленным типом.

Редактировать 2:

std::atomic слишком медленно на сайте чтения и записи. Я проверил это в моей системе. Запись происходит примерно в 30 раз медленнее, чтение примерно в 50 раз по сравнению с незащищенными примитивными операциями.

Ответы [ 4 ]

0 голосов
/ 10 января 2019

Сначала вы должны попробовать std::atomic, но убедитесь, что ваш компилятор знает и понимает вашу целевую архитектуру. Поскольку вы нацелены на Cortex-A15 (процессор ARMv7-A), обязательно используйте -march=armv7-a или даже -mcpu=cortex-a15.

Первый генерирует ldrexd инструкцию, которая должна быть атомарной в соответствии с документами ARM:

Один экземпляр атомарности

В ARMv7 доступ к элементарному процессору в единственном экземпляре:

  • все байтовые обращения
  • все обращения к полусловам в выровненные по полуслову локации
  • доступ ко всем словам в выровненные по словам местоположения
  • обращения к памяти, вызванные инструкциями LDREXD и STREXD в расположениях, выровненных по двойному слову.

Последний должен генерировать ldrd инструкцию, которая должна быть атомарной на целях, поддерживающих расширение большого физического адреса:

В реализации, включающей расширение большого физического адреса, LDRD и STRD доступы к 64-разрядным выровненным местоположениям являются 64-разрядными атомарными единичными копиями, что видно при просмотре таблиц перевода и доступе к таблицам перевода.

--- Примечание ---

Большое расширение физического адреса добавляет это требование, чтобы избежать необходимости в сложных мерах, чтобы избежать проблем атомарности при изменении записей таблицы перевода, без создания требования о том, что все расположения в системе памяти являются 64-разрядными атомарными единичными копиями.

Вы также можете проверить, как ядро ​​Linux реализует те:

#ifdef CONFIG_ARM_LPAE
static inline long long atomic64_read(const atomic64_t *v)
{
    long long result;

    __asm__ __volatile__("@ atomic64_read\n"
"   ldrd    %0, %H0, [%1]"
    : "=&r" (result)
    : "r" (&v->counter), "Qo" (v->counter)
    );

    return result;
}
#else
static inline long long atomic64_read(const atomic64_t *v)
{
    long long result;

    __asm__ __volatile__("@ atomic64_read\n"
"   ldrexd  %0, %H0, [%1]"
    : "=&r" (result)
    : "r" (&v->counter), "Qo" (v->counter)
    );

    return result;
}
#endif
0 голосов
/ 10 января 2019

Другим вариантом является наличие буфера неатомарных значений, которые выпускает издатель, и атомарного указателя на последние.

#include <atomic>
#include <utility>

template<class T>
class PublisherValue {
    static auto constexpr N = 32;
    T values_[N];
    std::atomic<T*> current_{values_};

public:
    PublisherValue() = default;
    PublisherValue(PublisherValue const&) = delete;
    PublisherValue& operator=(PublisherValue const&) = delete;

    // Single writer thread only.
    template<class U>
    void store(U&& value) {
        T* p = current_.load(std::memory_order_relaxed);
        if(++p == values_ + N)
            p = values_;
        *p = std::forward<U>(value);
        current_.store(p, std::memory_order_release); // (1) 
    }

    // Multiple readers. Make a copy to avoid referring the value for too long.
    T load() const {
        return *current_.load(std::memory_order_consume); // Sync with (1).
    }
};

Это без ожидания , но есть небольшая вероятность того, что считыватель может быть отложен во время копирования значения и, следовательно, прочитать самое старое значение, пока оно было частично перезаписано. Увеличение N уменьшает этот риск.

0 голосов
/ 10 января 2019

Никто не может знать. Вы должны будете увидеть, документирует ли ваш компилятор какую-либо многопоточную семантику, которая будет гарантировать, что это будет работать, или посмотрите на сгенерированный ассемблерный код и убедите себя, что это будет работать. Имейте в виду, что в последнем случае всегда возможно, что более поздняя версия компилятора или другие параметры оптимизации или более новый ЦП могут нарушить код.

Я бы предложил проверить std::atomic с соответствующим memory_order. Если по какой-то причине это слишком медленно, используйте встроенную сборку.

0 голосов
/ 10 января 2019

Является ли эта единственная переменная просто целым числом, указателем или обычным типом старого значения, вы, вероятно, можете просто использовать std :: atomic .

...