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

Недавно у меня был пример кода, как показано ниже (реальный код намного сложнее). После просмотра доклада Ганса Бёма cppcon16 об атомарности я немного беспокоюсь о том, работает ли мой код.

produce вызывается одним потоком-производителем, а consume вызывается несколькими потоками-потребителями. Производитель обновляет данные только в порядковом номере, таком как 2, 4, 6, 8,..., но устанавливает нечетный порядковый номер, например 1, 3, 5, 7,... перед обновлением данных, чтобы указать, что данные могут быть грязными. Потребители также пытаются получить данные в той же последовательности (2, 4, 6,...).

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

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

Правильно ли я понимаю, что назначение данных (в процессе производства) можно переместить в «хранилище (n-1)» выше, чтобы потребитель читал поврежденные данные, но t == t2 все еще преуспевает?

struct S 
{
    atomic<int64_t> seq;
    // data members of primitive type int, double etc    
    ...
};

S s;

void produce(int64_t n, ...) // ... for above data members
{
    s.seq.store(n-1, std::memory_order_release); // indicates it's working on data members

    // assign data members of s
    ...

    s.seq.store(n, std::memory_order_release); // complete updating
}

bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
    auto t = s.load(std::memory_order_acquire);

    if (t == n)
    {
        // read fields
        ...

        auto t2 = s.load(std::memory_order_acquire);
        if (t == t2)
            return true;
    }        

    return false;
}

person Derek    schedule 10.10.2016    source источник
comment
Предостережение: структуры без блокировок, подобные этой, очень сложно правильно реализовать. Есть несколько ловушек: blog.memsql.com/common -подводные камни-при-записи-алгоритмы-блокировки   -  person Violet Giraffe    schedule 10.10.2016
comment
en.cppreference.com/w/cpp/atomic/ занимается именно этим вопросом. Это отвечает на него для вас?   -  person Michael Foukarakis    schedule 10.10.2016
comment
Майкл, ссылка cppref сильно отличается от моего вопроса. Проблема, которую я пытаюсь решить, заключается в том, что производитель обновляет одни и те же 'S s;' несколько раз, и я хочу убедиться, что потребители читают данные с указанным порядковым номером, если производитель не обновил данные до более высокого порядкового номера.   -  person Derek    schedule 10.10.2016
comment
@VioletGiraffe: этот шаблон синхронизации уже хорошо известен: это вариант Seqlock с одной записью.   -  person Peter Cordes    schedule 12.10.2016


Ответы (1)


Переупорядочивание во время компиляции по-прежнему может укусить вас при ориентации на x86, потому что компилятор оптимизирует, чтобы сохранить поведение программы на абстрактной машине C++, а не более сильное поведение, зависящее от архитектуры. Поскольку мы хотим избежать memory_order_seq_cst, изменение порядка разрешено.

Да, ваши магазины могут изменить порядок, как вы предлагаете. Ваши загрузки также могут быть переупорядочены с загрузкой t2, поскольку загрузка-приобретение — это только одна барьер. Для компилятора было бы законно полностью оптимизировать проверку t2. Если переупорядочение возможно, компилятору разрешается решить, что это происходит всегда, и применить правило «как если бы», чтобы сделать код более эффективным. (Текущие компиляторы обычно этого не делают, но это определенно разрешено текущим стандартом, как написано. См. заключение обсуждения по этому поводу со ссылками на предложения по стандартам.)

Варианты предотвращения повторного заказа:

  • Сделайте все хранилища/загрузки элементов данных атомарными с выпуском и приобретением семантики. (Загрузка последнего члена данных предотвратит загрузку t2 первой.)
  • Используйте барьеры (они же заборы), чтобы упорядочить все атомарные хранилища и неатомарные загрузки как группа.

    Как объясняет Джефф Прешинг, a mo_release забор — это не то же самое, что mo_release магазин, и нам нужен двунаправленный барьер. std::atomic просто перерабатывает имена std::mo_ вместо того, чтобы давать разные имена для заборов.

    (Кстати, неатомарные сохранения/загрузки действительно должны быть атомарными с mo_relaxed, потому что технически Undefined Behavior читать их вообще, пока они могут быть в процессе перезаписи, даже если вы решите не смотреть на то, что вы читаете. )


void produce(int64_t n, ...) // ... for above data members
{
    /*********** changed lines ************/
    std::atomic_signal_fence(std::memory_order_release);  // compiler-barrier to make sure the compiler does the seq store as late as possible (to give the reader more time with it valid).
    s.seq.store(n-1, std::memory_order_relaxed);          // changed from release
    std::atomic_thread_fence(std::memory_order_release);  // StoreStore barrier prevents reordering of the above store with any below stores.  (It's also a LoadStore barrier)
    /*********** end of changes ***********/

    // assign data members of s
    ...

    // release semantics prevent any preceding stores from being delayed past here
    s.seq.store(n, std::memory_order_release); // complete updating
}



bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
    if (n == s.seq.load(std::memory_order_acquire))
    {
        // acquire semantics prevent any reordering with following loads

        // read fields
        ...

    /*********** changed lines ************/
        std::atomic_thread_fence(std::memory_order_acquire);  // LoadLoad barrier (and LoadStore)
        auto t2 = s.seq.load(std::memory_order_relaxed);    // relaxed: it's ordered by the fence and doesn't need anything extra
        // std::atomic_signal_fence(std::memory_order_acquire);  // compiler barrier: probably not useful on the load side.
    /*********** end of changes ***********/
        if (n == t2)
            return true;
    }

    return false;
}

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

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


Кстати, вы используете метод синхронизации Seqlock, но только с одним писателем. У вас есть только часть последовательности, а не часть блокировки для синхронизации отдельных модулей записи. В версии с несколькими записями писатели брали бы блокировку перед чтением/записью порядковых номеров и данных. (И вместо того, чтобы использовать seq no в качестве аргумента функции, вы бы прочитали его из блокировки).

Документ для обсуждения стандартов C++ N4455 (об оптимизации атомов компилятором см. вторую половину моего ответа на Может ли num++ быть атомарным для 'int num'?) использует его в качестве примера.

Вместо ограждения StoreStore они используют хранилища выпусков для элементов данных в модуле записи. (С атомарными элементами данных, как я уже упоминал, требуется, чтобы это было действительно правильно).

void writer(T d1, T d2) {
  unsigned seq0 = seq.load(std::memory_order_relaxed);  // note that they read the current value because it's presumably a multiple-writers implementation.
  seq.store(seq0 + 1, std::memory_order_relaxed);
  data1.store(d1, std::memory_order_release);
  data2.store(d2, std::memory_order_release);
  seq.store(seq0 + 2, std::memory_order_release);
}

Они говорят о том, чтобы позволить второй загрузке считывателем порядкового номера потенциально переупорядочиваться с более поздними операциями, если компилятору это выгодно, и использовать t2 = seq.fetch_add(0, std::memory_order_release) в считывателе как потенциальный способ получить загрузку с семантикой выпуска. С текущими компиляторами я бы не рекомендовал это; вы, вероятно, получите операцию locked на x86, где способ, который я предложил выше, не имеет каких-либо (или каких-либо фактических барьерных инструкций, потому что только полные ограждения seq_cst нуждаются в инструкции на x86).

person Peter Cordes    schedule 10.10.2016
comment
@cmaster: Вы думаете, что релиз-магазин — это двусторонний барьер StoreStore, который заказывает все остальные магазины. Это не так, как Джефф Прешинг объясняет в этой статье, где он объясняет различие между релизным магазином и автономным atomic_thread_fence(mo_release), которое является барьером. Это хорошо обсуждаемый факт, что релиз-магазины — это только односторонние барьеры, и то же самое для эквайринг-загрузок, и статья Джеффа ссылается на несколько других статей/докладов, в которых это упоминается. - person Peter Cordes; 13.10.2016
comment
Ой. Да, конечно, вы правы. Извините за это, я удалил свой комментарий. К сожалению, я больше не могу удалить свой отрицательный голос :-( - person cmaster - reinstate monica; 13.10.2016
comment
@cmaster: Я нашел ошибку редактирования, которую нужно исправить, и расширил первый абзац, чтобы, возможно, будущие читатели не начинали с того же самого, что у вас сложилось неправильное впечатление. Так что теперь вы можете изменить свой голос :) - person Peter Cordes; 13.10.2016