Понимание memory_order_relaxed

Я пытаюсь понять специфику memory_order_relaxed. Я имею в виду эту ссылку: CPP Ссылка.

#include <future>
#include <atomic>

std::atomic<int*> ptr {nullptr};

void fun1(){
ptr.store(new int{0}, std::memory_order_relaxed);
}

void fun2(){
while(!ptr.load(std::memory_order_relaxed));
}

int main(){
std::async(std::launch::async, fun1);
std::async(std::launch::async, fun2);
}

Вопрос 1. В приведенном выше коде технически возможно, чтобы fun2 находился в бесконечном цикле, где он видит значение ptr как nullptr, даже если поток, который устанавливает ptr, завершил работу?

Если предположить, я изменяю код выше на что-то вроде этого:

#include <future>
#include <atomic>

std::atomic<int> i {0};
std::atomic<int*> ptr {nullptr};

void fun1(){
i.store(1, std::memory_order_relaxed);
i.store(2, std::memory_order_relaxed);
ptr.store(new int{0}, std::memory_order_release);

}

void fun2(){
while(!ptr.load(std::memory_order_acquire));
int x = i.load(std::memory_order_relaxed);
}

int main(){
std::async(std::launch::async, fun1);
std::async(std::launch::async, fun2);
}

Смежный вопрос: возможно ли в коде выше для fun2 увидеть значение атомарного i как 1 или оно гарантировано увидит значение 2?

3

Решение

Интересное наблюдение состоит в том, что в вашем коде нет фактического параллелизма; то есть fun1 а также fun2 работать последовательно, причина в том, что при определенных условиях (включая вызов std::async с std::launch::async политика запуска), std::future объект, возвращенный std::async имеет свой блок деструктора, пока не будет запущен вызов функции. Поскольку вы игнорируете возвращаемый объект, его деструктор вызывается до конца инструкции. Если бы вы поменяли два утверждения в main() (то есть запуск fun2 до fun1), ваша программа была бы поймана в бесконечном цикле, так как fun1 никогда бы не побежал.

это std::future поведение с ожиданием уничтожения несколько противоречиво (даже в рамках комитета по стандартам), и, поскольку я предполагаю, что вы не имели в виду это, я позволю себе переписать 2 утверждения в main для (оба примера):

auto tmp1 = std::async(std::launch::async, fun1);
auto tmp2 = std::async(std::launch::async, fun2);

Это откладывает фактическое std::future вернуть уничтожение объекта до конца main чтобы fun1 а также fun2 получить работать асинхронно.

технически возможно, чтобы fun2 находился в бесконечном цикле, где он видит значение ptr как nullptr, даже если поток, который устанавливает ptr, завершил работу?

Нет, это невозможно с std::atomic (на реальной платформе, как уже упоминалось в разделе комментариев). С неstd::atomic переменная, компилятор мог (теоретически) выбрать сохранение значения только в регистре, но std::atomic сохраняется и когерентность кэша распространяет значение в другие потоки. С помощью std::memory_order_relaxed здесь хорошо, если вы не разыменовываете указатель.

Возможно ли в коде выше для fun2 увидеть значение атомарного i как 1, или он уверен, что увидит значение 2?

Гарантируется увидеть значение 2 в переменной x,
fun1 хранит два разных значения в одной и той же переменной, но поскольку существует четкая зависимость, они не переупорядочиваются.

В fun1, ptr.store с std::memory_order_release предотвращает i.store(2) с std::memory_order_relaxed от движения ниже его барьера выпуска. В fun2, ptr.load с std::memory_order_acquire предотвращает i.load с std::memory_order_relaxed от продвижения через свой барьер приобретения. Это гарантирует, что x в fun2 будет иметь значение 2.

Обратите внимание, что с помощью std::memory_order_relaxed на все атомика, можно было бы увидеть x со значением 0, 1 или 2, в зависимости от относительного порядка доступа к атомарной переменной i в отношении ptr.store а также ptr.load,

2

Другие решения


По вопросам рекламы [email protected]