📚 Hub Books: Онлайн-чтение книгРазная литератураC++17 STL Стандартная библиотека шаблонов - Яцек Галовиц

C++17 STL Стандартная библиотека шаблонов - Яцек Галовиц

Шрифт:

-
+

Интервал:

-
+
1 ... 98 99 100 101 102 103 104 105 106 ... 121
Перейти на страницу:
exclusively.

lock is free.

Got exception 123

Got exclusive lock.

Как это работает

При просмотре документации С++ может показаться несколько странным факт существования разных классов мьютексов и абстракций блокировок RAII. Прежде чем рассматривать наш конкретный пример кода, подытожим все, что может предложить STL.

Классы мьютексов

Термин mutex расшифровывается как mutual exclusion (взаимное исключение). Чтобы предотвратить неуправляемое изменение одного объекта несколькими конкурирующими потоками, способное привести к повреждению данных, можно использовать объекты мьютексов. STL предоставляет разные классы мьютексов, которые хороши в разных ситуациях. Все они похожи в том, что имеют методы lock и unlock.

Когда некий поток первым вызывает метод lock() для мьютекса, который не был заблокирован ранее, он получает контроль над мьютексом. На данном этапе другие потоки будут блокироваться при вызове метода lock до тех пор, пока первый поток не вызовет снова метод unlock. Класс std::mutex может делать именно это.

В STL существует множество разных классов мьютексов (табл. 9.2).

Классы блокировок

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

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

Для управления памятью существуют вспомогательные классы unique_ptr, shared_ptr и weak_ptr. Они предоставляют очень удобный способ избежать утечек памяти. Такие классы существуют и для мьютексов. Простейшим из них является std::lock_guard. Его можно использовать следующим образом:

void critical_function()

{

  lock_guard<mutex> l {some_mutex};

  // критический раздел

}

Конструктор элемента lock_guard принимает мьютекс, для которого мгновенно вызывает метод lock. Весь вызов конструктора заблокируется до тех пор, пока тот не получит блокировку для мьютекса. При разрушении объекта он разблокирует мьютекс. Таким образом, понять цикл блокировки/разблокировки сложно, поскольку она происходит автоматически.

В STL версии C++17 предоставляются следующие разные абстракции блокировок RAII (табл. 9.3). Все они принимают аргумент шаблона, который будет иметь тот же тип, что и мьютекс (однако, начиная с C++17, компилятор может вывести этот тип самостоятельно).

В то время как lock_guard и scoped_lock имеют простейшие интерфейсы, которые состоят только из конструктора и деструктора, unique_lock и shared_lock более сложны, но и более гибки. В последующих примерах мы увидим, как еще их можно использовать, помимо простой блокировки.

Вернемся к коду примера. Хотя код запускался только в контексте одного потока, мы увидели, что он собирался использовать вспомогательные классы для блокировки. Псевдоним типа shrd_lck расшифровывается как shared_lock<shared_mutex> и позволяет блокировать экземпляр мьютекса несколько раз в коллективном режиме. До тех пор, пока существуют sl1 и sl2, никакие вызовы print_exclusive не могут заблокировать мьютекс в эксклюзивном режиме. Это все еще просто.

Теперь перейдем к эксклюзивным функциям блокировки, которые появились позднее в функции main:

int main()

{

  {

    shrd_lck sl1 {shared_mut};

    {

      shrd_lck sl2 {shared_mut};

      print_exclusive();

    }

    print_exclusive();

  }

  try {

    exclusive_throw();

  } catch (int e) {

    cout << "Got exception " << e << 'n';

  }

  print_exclusive();

}

Важная деталь — после возвращения из exclusive_throw функция print_exclusive снова может заблокировать мьютекс, несмотря на то что exclusive_throw завершила работу некорректно из-за генерации исключения.

Еще раз взглянем на функцию print_exclusive, поскольку в ней был использован странный вызов конструктора:

void print_exclusive()

{

  uniq_lck l {shared_mut, defer_lock};

  if (l.try_lock()) {

    // ...

  }

}

Мы предоставили shared_mut и defer_lock в качестве аргументов конструктора для unique_lock в данной процедуре. defer_lock — пустой глобальный объект, который послужит для выбора другого конструктора класса unique_lock, просто не блокирующего мьютекс. Позднее можно вызвать функцию l.try_lock(), которая не блокирует мьютекс. Если мьютекс уже был заблокирован, то можно сделать что-то еще. При полученной блокировке деструктор поможет выполнить уборку. 

Избегаем взаимных блокировок с применением std::scoped_lock

Если бы взаимные блокировки происходили на дорогах, то выглядели бы так (рис. 9.2).

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

В многопоточных программах такие ситуации должен разрешать программист. Однако слишком легко потерпеть неудачу, если программа сама по себе довольно сложная.

В этом примере мы напишем код, намеренно создающий взаимную блокировку. Затем увидим, как писать код, который получает те же ресурсы, что привели другой код к взаимной блокировке, но воспользуемся новым классом блокировки STL std::scoped_lock, появившимся в C++17 с целью избежать этой ошибки.

Как это делается

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

1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространств имен std и chrono_literals:

#include <iostream>

#include <thread>

#include <mutex>

using namespace std;

using namespace chrono_literals;

2. Затем создадим два объекта мьютексов, которые понадобятся для создания взаимной блокировки:

mutex mut_a;

mutex mut_b;

3. Чтобы создать взаимную блокировку с двумя ресурсами, нужны две функции. Одна пробует заблокировать мьютекс А, а затем и мьютекс В, а другая сделает это в противоположном порядке. Позволив обеим функциям приостановиться между блокировками, можно гарантировать, что код навсегда попадет во взаимную блокировку. (Это делается только для демонстрации. Программа, не содержащая команд по

1 ... 98 99 100 101 102 103 104 105 106 ... 121
Перейти на страницу:

Комментарии

Обратите внимание, что комментарий должен быть не короче 20 символов. Покажите уважение к себе и другим пользователям!

Никто еще не прокомментировал. Хотите быть первым, кто выскажется?