Синхронизация против расслабленной атомики - PullRequest
0 голосов
/ 13 сентября 2018

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

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

Вопрос в следующем: Как правильно обеспечить, чтобы расслабленные атомарные записи выполнялись перед чтением? Мой текущий код правильный? (Предположим, что функции и типы отображаются в конструкции библиотеки std, как и ожидалось.)

void* Allocator::Alloc(size_t bytes, size_t alignment)
{
    void* p = AlignedAlloc(bytes, alignment);
    AtomicFetchAdd(&allocatedBytes, AlignedMsize(p), MemoryOrder::Relaxed);
    return p;
}

void Allocator::Free(void* p)
{
    AtomicFetchSub(&allocatedBytes, AlignedMsize(p), MemoryOrder::Relaxed);
    AlignedFree(p);
}

size_t Allocator::GetAllocatedBytes()
{
    AtomicThreadFence(MemoryOrder::AcqRel);
    return AtomicLoad(&allocatedBytes, MemoryOrder::Relaxed);
}

И некоторые определения типов для контекста

enum struct MemoryOrder
{
    Relaxed = 0,
    Consume = 1,
    Acquire = 2,
    Release = 3,
    AcqRel = 4,
    SeqCst = 5,
};

struct Allocator
{
    void*  Alloc            (size_t bytes, size_t alignment);
    void   Free             (void* p);
    size_t GetAllocatedBytes();

    Atomic<size_t> allocatedBytes = { 0 };
};

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

Часть, которая действительно меня сбивает с толку, заключается в том, что в стандарте под [atomics.fences] все пункты говорят о захвате / атомном операторе, синхронизирующемся с разблокировочным / атомным оператором. Для меня совершенно непонятно, будет ли синхронный забор / атомная операция синхронизироваться с расслабленной атомной операцией в другом потоке. Если функция забора AcqRel буквально отображается на инструкцию mfence, похоже, что приведенный выше код будет в порядке. Однако мне трудно убедить себя, что стандарт гарантирует это. А именно,

4 Атомная операция А, которая является операцией освобождения атомарного объект M синхронизируется с ограждением получения B, если существует атомарная операция X на M такая, что X упорядочивается перед B и читает значение, записанное A, или значение, записанное любым побочным эффектом в последовательность выпуска во главе с А.

Это, кажется, проясняет, что забор не будет синхронизироваться с расслабленными атомными записями. С другой стороны, полное ограждение - это и ограждение освобождения, и ограждение приобретения, поэтому оно должно синхронизироваться с самим собой, верно?

2 Защитное ограждение A синхронизируется с ограждением приобретения B, если оно есть существуют атомарные операции X и Y, обе работают на некотором атомном объекте M, так что A секвенируется до X, X модифицирует M, Y секвенируется перед B, и Y читает значение, записанное X или значение, записанное любым побочный эффект в гипотетической последовательности релиза X возглавил бы, если бы были операции освобождения.

Сценарий описан как

  • Непоследовательные записи
  • Ограждение
  • X атомная запись
  • Y атомное чтение
  • B приобрести забор
  • Непоследовательные чтения (здесь будут видны непоследовательные записи)

Тем не менее, в моем случае у меня нет атомарной записи + атомарного чтения в качестве сигнала между потоками, и ограничение выпуска происходит с ограничением получения в потоке B. Так что на самом деле происходит

  • Непоследовательные записи
  • Ограждение
  • Б приобрести забор
  • Непоследовательные чтения

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

Ответы [ 3 ]

0 голосов
/ 14 сентября 2018

Допустим, вы порождаете поток A, который вызывает Allocator::Alloc(), а затем немедленно порождают поток B, который вызывает Allocator::GetAllocatedBytes().Эти два Allocator звонка теперь выполняются одновременно.Вы не знаете, какой из них на самом деле произойдет первым, потому что между ними нет порядка.Ваша единственная гарантия заключается в том, что либо Поток B увидит значение allocatedBytes до того, как Поток A изменит его, либо он увидит значение allocatedBytes после того, как Поток A изменит его.Вы не будете знать, какое значение видел поток B, пока не вернется GetAllocatedBytes().(По крайней мере, поток B не увидит полностью мусорное значение для allocatedBytes, потому что нет никакой гонки данных благодаря использованию расслабленной атомики.)

Вы, похоже, обеспокоены случаем, когда потокA достигает значения AtomicFetchAdd(), но по какой-то причине изменение не отображается, когда поток B вызывает AtomicLoad().Ну и что?Это ничем не отличается от результата, в котором GetAllocatedBytes() работает до AtomicFetchAdd().И это абсолютно верный результат.Помните, либо поток B видит измененное значение, либо нет.

Даже если вы измените все атомарные операции / ограждения на MemoryOrder::SeqCst, это не будет иметь никакого значения.В описанном мною сценарии поток B все еще может видеть измененное значение или неизмененное значение allocatedBytes, поскольку два вызова Allocator выполняются одновременно.

Пока вы настаиваете на вызове GetAllocatedBytes()в то время как другие потоки все еще вызывают Alloc() и Free(), это действительно самое большее, что вы можете ожидать.Если вы хотите получить более «точное» значение, просто не допускайте одновременных вызовов на Alloc() / Free() во время работы GetAllocatedBytes()!Например, если программа закрывается, просто присоединитесь ко всем остальным потокам перед вызовом GetAllocatedBytes().Это даст вам точное количество выделенных байтов при завершении работы.Стандарт C ++ даже гарантирует это, потому что завершение потока синхронизируется с вызовом join () .

0 голосов
/ 14 сентября 2018

Если ваш вопрос , как правильно убедиться, что расслабленные атомарные записи зафиксированы перед чтением этого же атомарного объекта? Ничего, это обеспечивается языком, [intro.multithread]:

Все модификации конкретного атомного объекта M происходят в каком-то определенном общем порядке, называемом порядком модификации в M.

Все темы видят один и тот же порядок модификации . Например, представьте, что распределение 2 происходит в 2 разных потоках, а затем вы читаете счетчик в третьем потоке.

В первом потоке атомарный элемент увеличивается на 1 байт, а расслабленное выражение чтения / изменения (AtomicFetchAdd) возвращает 0: счетчик сделал этот переход: 0-> 1.

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

Затем в третьем потоке вы выполняете расслабленную нагрузку. Единственными возможными значениями, которые могут быть загружены, являются 0,1 или 3. Загрузка невозможна. 2. Порядок модификации атома - 0 -> 1 -> 3. И поток наблюдения также увидит этот порядок модификации.

0 голосов
/ 13 сентября 2018

Это не будет работать должным образом, acq_rel порядок памяти разработан специально для операций с памятью CAS и FAA, которые «одновременно» читают и записывают атомарные данные. В вашем случае вы хотите обеспечить синхронизацию памяти перед загрузкой. Для этого Вам необходимо изменить порядок памяти Вас fetchAndAdd и fetchAndSub на acq_rel и Ваш груз на acquire. Это может показаться многим, но на x86 это очень мало (некоторые оптимизации компилятора), поскольку не генерирует никаких новых инструкций в коде. Как работает синхронизация с выпуском и выпуском, я рекомендую эту статью: http://preshing.com/20120913/acquire-and-release-semantics/

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

Из моего понимания атомарности в C ++ ослабленный порядок памяти имеет смысл при использовании в сочетании с другими атомарными операциями с использованием ограждений памяти. Например, в некоторых ситуациях атомарный a может храниться в непринужденной манере, поскольку атомарный b записывается с порядком освобождения памяти и так далее.

...