Я пытаюсь понять специфику 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?
Интересное наблюдение состоит в том, что в вашем коде нет фактического параллелизма; то есть 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
,