У меня возникли сомнения после прочтения вопроса Мьютекс C++20 с атомным ожиданием , а также Можно ли иногда использовать std::atomic вместо std::mutex в C++? хотя похоже, и мое сомнение в другом, я объясню это ниже:
Я хочу знать, безопасно ли вызывать функцию std::atomic::wait для синхронизации фрагмента кода, как многие предлагали, похоже, что можно использовать std::atomic::wait, но я верю что, возможно, это не совсем безопасно из-за риска возникновения тупиковой ситуации
Представьте себе следующий код ниже:
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<bool> lock = false;
void go(){
bool expected = false;
while(!lock.compare_exchange_strong(expected, true)) {
lock.wait(true);
expected = false;
}
std::cout << "Sincronizado" << std::endl;
lock = false;
lock.notify_all();
}
int main() {
std::vector<std::thread> threads;
threads.reserve(2);
for(int i = 0; i < 2; i++)
threads.emplace_back(go);
for(auto& thread : threads)
thread.join();
}
Пока все хорошо, возможно, вы уже видели это в нескольких вопросах Stackoverflow, но теперь подумайте о следующем сценарии:
while(!lock.compare_exchange_strong(expected, true))
lock = false;
while(!lock.compare_exchange_strong(expected, true))
lock.wait(true);
if
that lock is true FUTEX_WAIT
) the thread is preempted lock.notify_all();
Возможен ли такой сценарий или у меня паранойя?
Прошу прощения за путаницу, я использую Google Translate, если у вас есть вопросы, используйте комментарии.
Лукас П.
См. std::atomic::wait:
Выполняет атомарные операции ожидания. Ведет себя так, как будто неоднократно выполняет следующие действия:
- Сравните представление значения
this->load(order)
с представлениемold
.
- Если они равны, то блокируется до тех пор, пока
*this
не будет уведомленnotify_one()
илиnotify_all()
или тред не будет разблокирован ложно.- В противном случае возвращается.
Эти функции гарантированно возвращаются только в том случае, если значение изменилось, даже если базовая реализация разблокируется ложно.
wait
является атомарным, поэтому в худшем случае он разблокируется до того, как вы позвоните notify
, вы никогда не пропустите уведомление, как это возможно с условной переменной.
Этот набор операций не является полностью атомарным, и поэтому, несмотря на то, что утверждение: «Эти функции гарантированно возвращаются только в том случае, если значение изменилось», ничто не мешает им не вернуться (см. ситуацию, о которой я упоминал выше в Почта)
Я могу понять, почему оно возвращается только в том случае, если значение действительно было изменено после прочтения исходного кода, но я не мог понять, где можно избежать тупиковой ситуации, которую я предложил в сообщении.
Цитата неверно истолкована (почти наоборот). Уведомления игнорируются, если на момент принятия решения значение все еще остается old
. Таким образом, хорошей реализацией было бы сначала загрузить, а затем проверить уведомление, если это невозможно одновременно. Если встроенная функция платформы проверяет уведомления перед загрузкой, поведение необходимо исправить в классе-оболочке; Однако я не могу подсчитать стоимость.
Именно по этой причине системный вызов FUTEX_WAIT
принимает в качестве аргумента ожидаемое «старое» значение. Перед переходом в режим сна он проверяет, содержит ли слово фьютекса это значение, и если нет, он немедленно возвращается вместо перехода в режим сна.
Из справочной страницы futex(2):
FUTEX_WAIT
(начиная с Linux 2.6.0)Эта операция проверяет, что значение в слове фьютекса на который указывает адрес
uaddr
, все еще содержит ожидаемое значениеval
, и если да, то спит в ожиданииFUTEX_WAKE
операция над словом фьютекса. Нагрузка значение слова фьютекса представляет собой атомарный доступ к памяти (т. е. используя атомные машинные инструкции соответствующего архитектура). Эта нагрузка, сравнение с ожидаемое значение и переход в режим сна выполняются атомарно и полностью упорядочен по отношению к другим фьютексам операции над одним и тем же словом фьютекса. Если поток начинается спать, по этому фьютексу считается официантом. Если значение фьютекса не соответствуетval
, вызов завершается неудачей. сразу с ошибкойEAGAIN
.Цель сравнения с ожидаемым значением состоит в том, чтобы предотвратить потерю пробуждения. Если другой поток изменил значение слово фьютекса после того, как вызывающий поток решил заблокировать его на основании предыдущее значение, и если другой поток выполнил операцию FUTEX_WAKE (или подобное пробуждение) после изменения значения и до этого FUTEX_WAIT, то вызывающий поток будет наблюдать за значением переоденусь и не начну спать.
Итак, в вашем примере поток 2 введет системный вызов FUTEX_WAIT
с помощью val == true
. Поскольку фактическое значение lock
в этот момент равно false
, системный вызов фьютекса заметит это и вернется, не переходя в режим сна. Затем lock.wait()
также вернется, давая потоку 2 еще один шанс выполнить compare_exchange
и попытаться забрать блокировку себе.
Предположительно, ядро реализует это, предварительно добавляя текущий поток в список потоков, ожидающих фьютекса, а затем (после барьера памяти) перезагружая слово фьютекса. Если оно по-прежнему равно val
, то мы переводим нить в режим сна. Если нет, удаляем его из списка официантов и возвращаем.
Таким образом, предполагая, что другой поток собирается изменить значение, а затем вызвать FUTEX_WAKE
, в любом случае мы в безопасности. Если мы наблюдаем значение val
, мы знаем, что после нашей загрузки произошло любое сохранение другого значения. Поскольку мы добавили себя в список ожидающих перед загрузкой и поскольку любое событие пробуждения будет сигнализироваться после сохранения, это доказывает, что событие пробуждения произойдет после того, как мы окажемся в списке ожидающих, и поэтому мы не пропустим его.
Вы правы, я забыл эту деталь, касающуюся реализации Futex.
Это была одна из основных причин, по которой я открыл этот вопрос, потому что я считаю, что описание cppreference сбивает с толку, см. в исходном коде gcc (github.com/gcc-mirror/gcc/blob/master/libstdc++- v3/include/bits/…) вы можете видеть, что сам вызов не является на 100% атомарным (потому что нет атомарной проверки + операции ожидания, такой как сравнение + изменение [CAS]), то есть внутри std: :atomic::wait используется do- while и внутри блока do есть проверка if, если это if истинно, то блокировка инициируется системным вызовом Futex с операцией FUTEX_WAIT.