Наконец, функция empty() вообще не изменяет данные, так что она точно безопасна относительно исключений

В этом коде есть две возможности для взаимоблокировки из-за того, что пользовательский код вызывается, когда удерживается блокировка: копирующий или перемещающий конструктор (1), (3) и копирующий или перемещающий оператор присваивания (5) хранимых в стеке данных. И еще — operator new, который также мог бы быть определён пользователем. Если любая из этих функций вызовет функции-члены стека, в который вставляется или из которого удаляется элемент, либо затребует какую-либо блокировку в момент, когда удерживается блокировка, захваченная при вызове функции-члена стека, то может возникнуть взаимоблокировка. Однако было бы разумно возложить ответственность за это на пользователей стека; невозможно представить себе разумную реализацию операций добавления в стек и удаления из стека, которые не копировали бы данные и не выделяли память.

Поскольку все функции-члены используют для защиты данных класс std::lock_guard<>, их можно безопасно вызывать из любого количества потоков. Единственные небезопасные функции-члены — конструкторы и деструкторы, но эта проблема не особенно серьезна; объект можно сконструировать и уничтожить только один раз. Вызов функций-членов не полностью сконструированного или частично уничтоженного объекта — это всегда плохо, и к одновременности доступа отношения не имеет. Таким образом, пользователь должен гарантировать, что никакой другой поток не может обратиться к стеку, пока он не будет сконструирован полностью, и что любая операция доступа завершается до начала его уничтожения.

Хотя благодаря блокировке несколько потоков могут одновременно вызывать функции-члены стека, в каждый момент времени с ним реально работает не более одного потока. Такая сериализация потоков может снизить производительность приложения в случае, когда имеется значительная конкуренция за стек, — пока поток ожидает освобождения блокировки, он не выполняет никакой полезной работы. Кроме того, стек не предоставляет средств, позволяющих ожидать добавления элемента. Следовательно, если потоку нужно получить из стека элемент, то он должен периодически опрашивать его с помощью empty() или просто вызывать pop() и обрабатывать исключение empty_stack.

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

<p>6.2.2. Потокобезопасная очередь с блокировками и условными переменными</p>

В листинге 6.2 воспроизведен код потокобезопасной очереди из главы 4. Если стек построен по образцу std::stack<>, то очередь — по образцу std::queue<>. Но ее интерфейс также отличается от стандартного адаптера контейнера, потому что запись в структуру данных должна быть безопасной относительно одновременного доступа из нескольких потоков.

Листинг 6.2. Потокобезопасная очередь с блокировками и условными переменными

template

class threadsafe_queue {

private:

 mutable std::mutex mut;

 std::queue data_queue;

 std::condition_variable data_cond;

public:

 threadsafe_queue() {}

 void push(T new_value) {

  std::lock_guard lk(mut);

  data_queue.push(std::move(data));

  data_cond.notify_one(); ←(1)

 }

 void wait_and_pop(T& value) { ←(2)

  std::unique_lock lk(mut);

  data_cond.wait(lk, [this]{return !data_queue.empty();});

  value = std::move(data_queue.front());

  data_queue.pop();

 }

 std::shared_ptr wait_and_pop() ←(3)

  std::unique_lock lk(mut);

  data_cond.wait(lk, [this] {return !data_queue.empty();});←(4)

  std::shared_ptr res(

   std::make_shared(std::move(data_queue.front())));

  data_queue.pop();

  return res;

 }

 bool try_pop(T& value) {

  std::lock_guard lk(mut);

  if (data_queue.empty())

   return false;

  value = std::move(data_queue.front());

Перейти на страницу:

Похожие книги