В следующем коде реализована некоторая свободная от блокировок (и атомарная!) связь между потоками, которая требует использования барьеров сохранения и загрузки памяти, но семантика освобождения-получения C++11 не подходит и не гарантирует правильность. На самом деле алгоритм выявляет потребность в своего рода инверсии семантики освобождения-приобретения, то есть для того, чтобы сигнализировать о том, что какая-то операция не произошла, а не произошла.
volatile bool valid=true;
volatile uint8_t blob[1024] = {/*some values*/};
void zero_blob() {
valid=false;
STORE_BARRIER;
memset(blob,0,1024);
}
int32_t try_get_sum(size_t index_1, size_t index_2) {
uint8_t res = blob[index_1] + blob[index_2];
LOAD_BARRIER;
return valid ? res : -1;
}
Я могу сделать этот код правильным на всех аппаратных архитектурах, просто используя встроенные барьеры памяти, например. на Intel здесь нет необходимости в барьерах памяти, на Sparc (RMO) membar #StoreStore и membar #LoadLoad, на PowerPC lwsync для обоих. Так что ничего страшного, и код является типичным примером использования барьеров хранения и загрузки. Теперь, какую конструкцию С++ 11 я должен использовать, чтобы сделать код правильным, предполагая, что я не хочу преобразовывать «BLOB» в объекты std::atomic
, так как это сделало бы «BLOB» защитным объектом, а переменную «valid» защищенным, тогда как это наоборот. Преобразование переменной «valid» в объект std::atomic
для меня нормально, но нет никаких барьеров, гарантирующих правильность. Чтобы было понятно, рассмотрим следующий код:
volatile std::atomic<bool> valid{true};
volatile uint8_t blob[1024] = {/*some values*/};
void zero_blob() {
valid.store(false, std::memory_order_release);
memset(blob,0,1024);
}
int32_t try_get_sum(size_t index_1, size_t index_2) {
uint8_t res = blob[index_1] + blob[index_2];
return valid.load(std::memory_order_acquire) ? res : -1;
}
Код неверен, так как барьеры размещены не в тех местах, и, следовательно, запись в «большой двоичный объект» может предшествовать записи в «действительный» и/или загрузка из «действительного» может предшествовать загрузке из «большого двоичного объекта». Я думал, что для работы с такими конструкциями C++11 предоставил std::atomic_thread_fence
и код должен быть таким:
volatile std::atomic<bool> valid{true};
volatile uint8_t blob[1024] = {/*some values*/};
void zero_blob() {
valid.store(false, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release);
memset(blob,0,1024);
}
int32_t try_get_sum(size_t index_1, size_t index_2) {
uint8_t res = blob[index_1] + blob[index_2];
std::atomic_thread_fence(std::memory_order_acquire);
return valid.load(std::memory_order_relaxed); ? res : -1;
}
К сожалению, С++ 11 говорит:
Ограждение освобождения A синхронизируется с ограждением получения B, если существуют атомарные операции X и Y, обе из которых работают с некоторым атомарным объектом M, так что A упорядочена до X, X изменяет M, Y упорядочена до B, а Y считывает значение записанное X или значение, записанное любым побочным эффектом в гипотетической последовательности освобождения X, если бы это была операция освобождения.
в котором четко указано, что std::atomic_thread_fence
следует размещать в противоположных сторонах операций над атомарным объектом.
ПОЗЖЕ РЕДАКТИРОВАТЬ
Ниже вы найдете гораздо более полезный пример:
volatile uint64_t clock=1;
volatile uint8_t blob[1024] = {/*some values*/};
void update_blob(uint8_t vals[1024]) {
clock++;
STORE_BARRIER;
memcpy(blob,vals,1024);
STORE_BARRIER;
clock++;
}
int32_t try_get_sum(size_t index_1, size_t index_2) {
uint64_t snapshot = clock;
if(snapshot & 0x1) {
LOAD_BARRIER;
uint8_t res = blob[index_1] + blob[index_2];
LOAD_BARRIER;
if(snapshot == clock)
return res;
}
return -1;
}
memset
тинг и чтениеblob[index]
одновременно? Стандарт не говорит, чтоres
будет неопределенным, тогда он четко говорит, что это UB. Конечно, это может работать с вашей реализацией/средой, но я бы не советовал такой код. - person Daniel Langr   schedule 05.09.2019volatile
бесполезен для любого вашего кода. Если вы создаетеatomic
значениеvolatile
, то вы почти наверняка делать неправильно. - person Nicol Bolas   schedule 05.09.2019volatile std::atomic<bool>
ну... это новинка в моем списке неправильного использования volatile - person Guillaume Racicot   schedule 05.09.2019volatile
не помогает коду достичь своей цели, и его присутствие убедительно свидетельствует о том, что автор страдает от распространенной ошибки: вера в то, чтоvolatile
имеет какое-либо отношение к видимости или потокобезопасности действия. - person Nicol Bolas   schedule 06.09.2019atomic_thread_fence(std::memory_order_release);
Как релиз скажет что-нибудь о том, чего еще не было? Освобождение означает, что прошлое прошло; это создает прошлое. Это не создает будущее (приобретает). - person curiousguy   schedule 06.09.2019atomic
версия) очень сильно ожидает, чтоvolatile
сделает код потокобезопасным , я бы не стал делать такое предположение. Тот факт, что пользователь сохранилvolatile
в версииatomic
, также подтверждает идею о том, что ОП, вероятно, не понимает, что это не помогает. - person Nicol Bolas   schedule 06.09.2019try_get_sum
не выполнялся, покаmemcpy
все еще выполняется. И чтобыmemcpy
не начал выполняться, пока другой поток читает данные. - person Nicol Bolas   schedule 06.09.2019volatile
(почти) бесполезен для независимого от платформы многопоточного программирования приложений. Он не обеспечивает никакой синхронизации, не создает ограждений памяти и не обеспечивает порядок выполнения операций. Он не делает операции атомарными. Это не делает ваш код волшебным образом потокобезопасным.volatile
может быть единственным неправильно понятым средством во всем C++. Другой ответ по этой теме. - person Daniel Langr   schedule 13.09.2019