- 01
- 02
- 03
- 04
- 05
- 06
- 07
- 08
- 09
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
#include <cstdlib>
#include <chrono>
#include <iostream>
#include <thread>
int p = 0;
int *q = nullptr;
void g()
{
using namespace std::chrono_literals;
std::cout << "g()" << std::endl;
std::cout << "g(): p = 1" << std::endl;
p = 1;
std::this_thread::sleep_for(1s);
if (q != nullptr) {
std::cout << "g(): *q = 1" << std::endl;
*q = 1;
} else {
std::cout << "g(): q == nullptr" << std::endl;
}
}
void f()
{
using namespace std::chrono_literals;
std::cout << "f()" << std::endl;
if (p == 0) {
std::cout << "f(): first loop start" << std::endl;
while (p == 0) { } // Потенциально конечный
std::cout << "f(): first loop end" << std::endl;
}
int i = 0;
q = &i;
std::cout << "f(): second loop start" << std::endl;
while (i == 0) { } // Потенциально конечный, хотя в условии только автоматическая пельменная
std::cout << "f(): second loop end" << std::endl;
}
int main()
{
using namespace std::chrono_literals;
std::cout << "f() thread start" << std::endl;
auto thr1 = std::thread(f);
thr1.detach();
std::this_thread::sleep_for(1s);
std::cout << "g() thread start" << std::endl;
auto thr2 = std::thread(g);
thr2.detach();
std::this_thread::sleep_for(2s);
std::cout << "Done" << std::endl;
std::_Exit(EXIT_SUCCESS);
}
Реальный пример: https://ideone.com/OCJQg9 («Wandbox» лежит, зараза).
А чтобы получить ожидаемый вывод — надо конпелировать с «-O0».
Выхлоп компилятора прекрасен (чуть упрощённый кот): https://gcc.godbolt.org/z/Kx4xeE
1.10 Multi-threaded executions and data races
The implementation may assume that any thread will eventually do one of the following:
— terminate,
— make a call to a library I/O function,
— access or modify a volatile object, or
— perform a synchronization operation or an atomic operation.
В f() нет ни I/O, ни доступа к волатильным объектам, ни атомарных операций.
Какой карманный лев )))
Это на x86 мы привыкли к полной когерентности, а есть ведь платформы с более слабой моделью памяти, где проц тупо не будет перезагружать кешлайн, в котором лежит p или i. Ты ведь не дал ему такого повода.
Это важно для обращения ко всяким железкам, когда порядок записей и даже чтений имеет значение.
Ну и в обработчике прерываний, если у тебя с ним какие-то переменные расшарены.
Every access (read or write operation, member function call, etc.) made through a glvalue expression of volatile-qualified type is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution
Опять же, я считаю это не совсем правдой. volatile можно использовать для синхронизации если старая-добрая одноядерная машина.
>у нас в жабе всё проще
Я бы не сказал. Оно везде сложно.
Они в 8ой вроде fence из крестов навезли.
Они из java memory model его и заимствовали :)
Но в лучших традициях С++, они поступили согласно принципу: давайте возьмём концепт и сделаем его в 10 раз сложнее.
Просто в Йажа volatile был full fence. Крестобляди рассудили что это черезчур медленно и черезчур просто и добавили 4 разных memory_order.
Но она хуёво ложится на разнообразие железа и низкоуровневых инструкций. (MFENCE, LFENCE, SFENCE)
И не всегда имеет оптимальный пирформанс, накладывая своими гарантиями лишние ограничения на компилятор.
Во многих случаях достаточно memory_order_relaxed.
Именно поэтому я за «C++».
угу)
ну это же вечный трейдофф: "много думать" versus тормоза.
Вот в питоне люди юзают GIL, и им куда проще жить
когда вообещ в кресты завезли мемори модел для тредов? C++11?
> если старая добрая одноядерная машина
Нет. Разве что в примитивных случаях, где relaxed хватает. Насколько помню, volatile не является конпеляторным барьером и не запрещает переупорядочивать обычные записи вокруг себя. Только другие volatile и сайд-эффекты. Т.е. тот же мутекс из волатайла получится весьма хуёвый.
Да.
> Разве что в примитивных случаях, где relaxed хватает.
Да. Именно тот пример, который я привёл. Когда переменная меняется один раз за всё время работы программы.
Даже пытаться использовать его как атомик — путь в ад.
>не является конпеляторным барьером
запись в волатилку всегда имеет сайд эффект, а значит код ПОСЛЕ него должен быть реально исполнен после этой записи, не?
Любому вменяемому человеку понятно что использование volatile в качестве примитива синхронизации — полная хуйня.
Особенно в свете выхода С++11 и появления широкого набора кроссплатформенных альтернатив.
Спасибо что напомнил, я когда-то хотел попробовать, но у меня не было подходящего железа.
Вика пишет, что Dependent loads can be reordered, хотя мне это не очень понятно. Видимо бывают сорта депенденсов.
А что у ваших армов с кококококогерентностью кеша? попадание в кеш-то гарантировано бродкастится во все кеши, или тоже нужно явно?
В джаве вроде бы нет, потому volatile сделает fence.
То получится вот это:
Рекомендую почитать JCP, там все эти вопросы очень подробно разобраны.
оказалось, что это тот же неймспейс
https://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange
Я уже вроде приводил контр-пример.
То, что на интеле всё когерентно и в атомик риде нет барьеров и специальных инструкций, поэтому и обычное чтение сойдёт?
>То, что на интеле всё когерентно
А на других платформах разве будут проблемы?
У нас есть признак шатдауна. Он может поменяться только одним способом (из 0 в 1).
Как только он стал ненулевым рано или поздно все это заметят и остановятся.
Если у тебя там какая-то хуйня с тыщей ядер, то им будет очень дорого следить за кешами друг-друга. В лучшем случае они будут мониторить только реальные записи в память (т.е. тебе понадобится write-through семантика на done = 1). В худшем случае они вообще ничего не будут мониторить (и тогда нужно чтение с инвалидацией кеша на !done). volatile ничего из этого не даёт.
Я к тому что атомик-чтения и особенно мьютексы были бы слишком дорогими в этой ситуации с флагом.
volatile раньше был вполне адекватным методом.
Но в целом после завоза в кресты std::memory_order и happens-before семантики оно бесполезно.
Когда юзер психанёт и ткнёт в резет, ага. Представь, что это были ядерные треды, которые никто никогда не вытеснит.
> слишком дорогими
Да вот нихуя. Либо атомарное чтение бесплатное уже включено в цену (интел, арм) либо без него твой код тупо не работает (альфа?)
Я вижу только одну ситуацию, что они не увидят друг-друга: в какой-то NUMA машине из нескольких сокетов. Но в таких обычно стараются делать NUMA-aware пулы чтобы потоки взаимодействовали в рамках своего сокета.
>атомарное чтение уже включено в цену (интел, арм)
Разве? Там же лишний LFENCE будет на каждом чтении, не?
Зачем? Зачем? Посмотри на годболте во что атомарное чтение раскрывается.
На арме вроде тоже чтение бесплатно, а вот в записи барьер. Но я не изучал их модель.
Вон, я выше по ветке привёл реальный пример. В «mov eax, DWORD PTR p[rip]» раскрывается.
Подтверждаю.
Так читаешь код, и даже не уверен в его атомарности.
В Йажа точно то же.
https://openjdk.java.net/jeps/171
В ваших крестах с блядскими перегрузочками нихера не понятно. Какие именно гарантии у этого чтения?
Пиши явно: while (!p.load(std::memory_order_relaxed)), тогда бесплатно. Для флажка об остановке релакса должно хватить.
Так а они ведь как-то видят изменения volatile-переменных после прерываний.
Прерывание происходит на том же самом ядре, со своим собственным кешем проблем не будет.
А для MMIO с железками как правило write back отключен, поэтому проц всё честно пишет.