Можно ли использовать std::atomic::wait для замены мьютекса?

У меня возникли сомнения после прочтения вопроса Мьютекс 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, но теперь подумайте о следующем сценарии:

Тема 1:

+ Win the competition while(!lock.compare_exchange_strong(expected, true))
+ Get to the line before executing lock = false;
+ Thread 1 is preempted

Тема 2:

+ Lose the competition while(!lock.compare_exchange_strong(expected, true))
+ Start executing the line lock.wait(true);
+ Inside the line (which internally uses Futex) it checks if that lock is true
+ Still within the line, before blocking begins (FUTEX_WAIT) the thread is preempted

Тема 1:

+ Return to execution, and set lock to false.
+ Notifies all pending threads lock.notify_all();

Тема 2:

+ Return to execution, blocking begins (FUTEX_WAIT)
+ A possible dead-lock?

Возможен ли такой сценарий или у меня паранойя?

Прошу прощения за путаницу, я использую Google Translate, если у вас есть вопросы, используйте комментарии.

ЖДАТЬ !

If my question is incomplete, or is formatted in an inappropriate way, please comment so I can adjust it, thank you in advance, in advance.

Лукас П.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
106
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

См. std::atomic::wait:

Выполняет атомарные операции ожидания. Ведет себя так, как будто неоднократно выполняет следующие действия:

  • Сравните представление значения this->load(order) с представлением old.
    • Если они равны, то блокируется до тех пор, пока *this не будет уведомлен notify_one() или notify_all() или тред не будет разблокирован ложно.
    • В противном случае возвращается.

Эти функции гарантированно возвращаются только в том случае, если значение изменилось, даже если базовая реализация разблокируется ложно.

wait является атомарным, поэтому в худшем случае он разблокируется до того, как вы позвоните notify, вы никогда не пропустите уведомление, как это возможно с условной переменной.

Это была одна из основных причин, по которой я открыл этот вопрос, потому что я считаю, что описание 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.

Lucas Paixão 28.04.2024 10:48

Этот набор операций не является полностью атомарным, и поэтому, несмотря на то, что утверждение: «Эти функции гарантированно возвращаются только в том случае, если значение изменилось», ничто не мешает им не вернуться (см. ситуацию, о которой я упоминал выше в Почта)

Lucas Paixão 28.04.2024 10:49

Я могу понять, почему оно возвращается только в том случае, если значение действительно было изменено после прочтения исходного кода, но я не мог понять, где можно избежать тупиковой ситуации, которую я предложил в сообщении.

Lucas Paixão 28.04.2024 10:51

Цитата неверно истолкована (почти наоборот). Уведомления игнорируются, если на момент принятия решения значение все еще остается old. Таким образом, хорошей реализацией было бы сначала загрузить, а затем проверить уведомление, если это невозможно одновременно. Если встроенная функция платформы проверяет уведомления перед загрузкой, поведение необходимо исправить в классе-оболочке; Однако я не могу подсчитать стоимость.

Red.Wave 28.04.2024 17:17
Ответ принят как подходящий

Именно по этой причине системный вызов 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.

Lucas Paixão 28.04.2024 18:35

Другие вопросы по теме