У меня есть список, из которого я хочу, чтобы разные темы брали элементы. Во избежание блокировки мьютекса, защищающего список, когда он пуст, я проверяю empty()
перед блокировкой.
Это нормально, если вызов list::empty()
100% времени неправильный. Я только хочу , чтобы избежать сбоя или нарушений одновременно list::push()
и list::pop()
вызовов.
Могу ли я предположить, что VC ++ и Gnu GCC только иногда empty()
ошибаются и ничего хуже?
if(list.empty() == false){ // unprotected by mutex, okay if incorrect sometimes
mutex.lock();
if(list.empty() == false){ // check again while locked to be certain
element = list.back();
list.pop_back();
}
mutex.unlock();
}
c++
multithreading
optimization
race-condition
stdlist
Энн Куинн
источник
источник
std::list::size
это гарантирует постоянную сложность по времени, что в основном означает, что размер (количество узлов) необходимо хранить в отдельной переменной; давайте назовем этоsize_
.std::list::empty
затем, вероятно, возвращает что-то какsize_ == 0
, и одновременное чтение и записьsize_
вызовут гонку данных, следовательно, UB.Ответы:
Нет, это не хорошо. Если вы проверите, является ли список пустым вне какого-либо механизма синхронизации (блокирующего мьютекс), тогда у вас есть гонка данных. Гонка данных означает, что у вас неопределенное поведение. Наличие неопределенного поведения означает, что мы больше не можем рассуждать о программе, и любой вывод, который вы получаете, является «правильным».
Если вы цените здравомыслие, перед проверкой вы поймете производительность и заблокируете мьютекс. Тем не менее, список может даже не быть правильным контейнером для вас. Если вы сможете точно сообщить нам, что вы делаете с ним, мы могли бы предложить лучший контейнер.
источник
list::empty()
является действием чтения , которое не имеет ничего общего сrace-condition
read-write
илиwrite-write
. Если вы не верите мне, прочитайте стандартный раздел о гонках данныхСуществует чтение и запись (скорее всего, для
size
членаstd::list
, если мы предположим, что он назван так), которые не синхронизированы друг с другом . Представьте, что один поток вызываетempty()
(в вашем внешнемif()
), в то время как другой поток входит во внутреннийif()
и выполняетpop_back()
. Затем вы читаете переменную, которая, возможно, изменяется. Это неопределенное поведение.источник
В качестве примера того, как все может пойти не так:
Достаточно умный компилятор мог видеть, что
mutex.lock()
не может изменитьlist.empty()
возвращаемое значение и, таким образом, полностью пропустить внутреннююif
проверку, что в конечном итоге привело к появлениюpop_back
в списке, в котором последний элемент был удален после первогоif
.Почему это может сделать это? Синхронизация отсутствует
list.empty()
, поэтому, если бы она была изменена одновременно, это привело бы к гонке данных. Стандарт гласит, что в программах не должно быть гонок данных, поэтому компилятор примет это как должное (в противном случае он может почти не выполнять оптимизацию). Следовательно, он может предполагать однопоточный подход к несинхронизированнымlist.empty()
и делать вывод, что он должен оставаться постоянным.Это только одна из нескольких оптимизаций (или поведения оборудования), которые могут сломать ваш код.
источник
a.load()+a.load()
...a.load()*2
очевидна? Дажеa.load(rel)+b.load(rel)-a.load(rel)
не оптимизирован в любом случае. Ничего. Почему вы ожидаете, что блокировки (которые по сути имеют последовательность seq) будут более оптимизированы?a.load() + a.load()
к2 * a.load()
. Не стесняйтесь задавать вопрос об этом, если вы хотите узнать больше.