Потенциальная тупик в поточно-ориентированном стеке C ++ - PullRequest
3 голосов
/ 04 мая 2020

В книге «Параллелизм в действии» есть реализация многопоточного стека, в котором мьютекс получается / блокируется при входе в функции pop () и empty (), как показано ниже:

class threadsafe_stack {
   private:
      std::stack<T> data;
      mutable std::mutex m;
   public:
      //...
      void pop(T& value) {
         std::lock_guard<std::mutex> lock(m);
         if(data.empty()) throw empty_stack();
         value = std::move(data.top());
         data.pop();
      }

      bool empty() const {
         std::lock_guard<std::mutex> lock(m);
         return data.empty();
      }
};

My Вопрос в том, как этот код не заходит в тупик, когда поток, получивший блокировку при вводе pop (), вызывает empty (), который также защищен мьютексом? Если lock () вызывается потоком, который уже владеет мьютексом, разве это не неопределенное поведение?

Ответы [ 3 ]

2 голосов
/ 04 мая 2020

как этот код не попадает в тупик, когда поток, получивший блокировку при входе в pop (), вызывает empty (), который также защищен мьютексом?

Поскольку вы не вызываете empty функцию-член threadsafe_stack, но вы вызываете empty () класса std::stack<T>. Если бы код был:

void pop(T& value) 
{
    std::lock_guard<std::mutex> lock(m);
    if(empty()) // instead of data.empty()
        throw empty_stack();
    value = std::move(data.top());
    data.pop();
}

Тогда это было бы неопределенное поведение :

Если блокировка вызывается потоком, который уже владеет мьютексом , поведение не определено: например, программа может зайти в тупик. Реализация, которая может обнаружить недопустимое использование, рекомендуется генерировать вместо тупиковой блокировки std :: system_error с условием ошибки resource_deadlock_would_occur.

Узнать о рекурсивных и shared мьютекс.

1 голос
/ 04 мая 2020

Не уверен на 100%, что вы имеете в виду, я думаю, вы имеете в виду последовательный вызов pop и empty в одной и той же теме? Как и в

while(!x.empty()) x.pop();

std::lock_guard, следует RAII. Это означает, что конструктор

std::lock_guard<std::mutex> lock(m);

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

Внутри pop вызывается только data.empty(), который не защищен мьютексом. Вызов this->empty() внутри pop действительно приведет к неопределенному поведению.

1 голос
/ 04 мая 2020

Вы были бы правы, если бы pop позвонил бы this->empty. Двойная блокировка одного и того же мьютекса с помощью std::lock_guard является неопределенным поведением, если только заблокированный мьютекс не является рекурсивным.

From cppreference в конструкторе (который используется в примере кода):

Эффективно вызывает m.lock (). Поведение не определено, если m не является рекурсивным мьютексом, и текущему потоку уже принадлежит m.

Для полноты картины существует второй конструктор:

lock_guard( mutex_type& m, std::adopt_lock_t t );

, который

Приобретает мьютекс m без попытки его блокировки. Поведение не определено, если текущему потоку не принадлежит m.

Однако pop вызывает data.empty, и это метод закрытого члена, а не функция-члена empty of threadsafe_stack. В коде нет проблем.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...