[БЕЗ_ЗВУКА] Итак, в предыдущем видео мы с вами стали свидетелем того, как семья из четырех человек смогла разорить банк из-за отсутствия синхронизации доступа к балансу денег на счету. И, на самом деле, этот пример демонстрирует худший вариант развития событий. У нас была нарушена целостность данных, при этом программа корректно отработала, корректно завершилась, и только посмотрев на ее вывод, мы смогли понять, что у нас что-то не так, что денег было потрачено больше, чем можно. На самом деле, может быть, все не так плохо и при отсутствии синхронизации доступа программа может падать. Давайте посмотрим пример: допустим, добавим в наш класс Account вектор history, который будет сохранять все транзакции, то есть мы туда будем добавлять суммы денег, которые мы списывали со счёта. Мы добавили две строчки ровно, и вот наша ситуация, где четверо членов одной семьи тратят деньги. Давайте скомпилируем новый код, запустим, и он чуть-чуть подумает и упадет. Почему? Я думаю, вы даже сами понимаете, что здесь происходит. У нас несколько тредов добавляют вектор, когда у вектора кончилась емкость во время выполнения push back, он выполняет реолокацию, и у нас несколько тредов параллельно аллоцируют новую память, какой-то из них свою память записывает во внутренности вектора, выполняет какие-то там перемещения данных. Это никак не синхронизировано. То есть треды параллельно обращаются к одним и тем же данным. И программа падает, потому что мы с памятью делаем что-то страшное. Как же нам этого избежать, вот таких проблем? Нам нужно воспользоваться mutex — это классический способ синхронизации потоков. При этом mutex — это не что-то такое, название такое экзотическое, это не что-то такое специфичное, исключительное для многопоточного программирования. На самом деле mutex — это аббревиатура, происходит от английского словосочетания MUtual EXclusion — взаимные исключения. И в жизни mutex встречаются тоже довольно часто. И простым примером mutex является перекресток, регулируемый светофором. Смотрите, у нас есть крестообразной перекресток. Здесь есть два потока движения: вертикальный поток движения и горизонтальный поток движения. Сейчас обратите внимание, у нас горит зеленый светофор, разрешающий перемещение для вертикального потока движения. Поэтому у нас вдоль вертикального потока свободно двигаются автомобили, а горизонтальный поток заблокирован, заблокирован светофором, который горит красным. Хорошо. В какой-то момент у нас наш светофор переключается и начинает в обе стороны гореть красным, и вертикальный поток заблокирован, и горизонтальный поток заблокирован. Дальше светофор загорается зеленым для горизонтального потока, и горизонтальный поток движется, вертикальный поток заблокирован. При этом наш мьютекс, наш светофор, он защищает эту область, которая называется перекрестком, и в программировании эта область называется критической секцией. Критическая секция — это тот участок кода, который в любой момент времени может выполнять не более одного потока. В данном случае в примере с перекрестком в любой момент времени здесь может двигаться не более одного потока машин: либо вертикальный поток машин, либо горизонтальный поток машин. Если и тот, и тот, будут двигаться одновременно, произойдет авария. В случае с многопоточными программами, если в критической секции оказывается более одного потока, то значит, результаты оказываются фатальные. Мы видели, у нас либо нарушалась целостность данных, либо программа падала. Хорошо, мы рассмотрели пример с перекрестком, теперь давайте применим mutex в нашей программе. Для этого подключим заголовочный файл mutex и в классе Account объявим поле типа mutex и назовем его m. Смотрите, критической секцией в нашем коде является метод spend, потому что он обращается к методу balance. И именно вот это поле нашего класса, оно является разделяемыми данными, к нему обращаются несколько потоков. То есть критическая секция — это, в общем-то, весь метод spend, и вот этот метод нужно защитить mutex'ом. Как это делается? Мы берем и в начале метода пишем lock guard mutex. Вы видите, что lock_guard это шаблон. Допустим, назовем его g и в качестве параметра передаем ему наш mutex m. Давайте для начала скомпилируем. Оно компилируется, запустим и видим, что наша семья из четырех человек теперь тратит ровно столько денег, сколько был на счету, 100 000. Давайте запустим несколько раз и все несколько раз у нас целостность данных не нарушается. Тем более мы теперь еще и историю считаем. У нас программа падала, теперь у нас всё отлично работает, корректно. Программа не падает, целостность данных не нарушается. Давайте запустим отладчик и продемонстрируем, что действительно у нас происходит взаимное исключение, то есть в критической секции в любой момент времени может находиться только один поток. Для этого мы сделаем вот что. Мы зайдем внутрь метода spend, поставим внутри if break point, и вот здесь есть break point properties, Common, и вот здесь есть ignore count, мы поставим 50 000. У нас всего 100 000 денег на счету, мы поставим 50 000. То есть исполнение программы 50 000 должно через эту строчку проскочить, и на 50 001-й наш break point сработает. Это нужно потому что, если мы не поставим этот ignore count, то смотрите, мы создали первый future здесь с помощью async, и функция SpendMoney уже стала работать. И наш break point сработает уже в этот момент раньше, чем мы создадим еще потоки, которые будут выполняться и тоже снимать деньги со счета. Поэтому мы поставили 50 000, чтобы точно, когда наш break point сработает, мы знали, чтобы обеспечить, что там уже будут все треды запущены и будет действительно параллельный доступ к переменной balance. Запускаем наш код, он начинает выполняться, и нам надо немного подождать, пока наш break point сработает, потому что все-таки такой условный break point, он немножко замедляет выполнение программы. Итак, мы дождались, и наш break point сработал. Давайте посмотрим, в каком состоянии находится наши программа. И для этого мы повнимательнее посмотрим в это окно, где у нас изображены потоки нашего приложения. Что мы видим? Что поток номер три в нашем приложении находится в методе spend нашего класса Account. Отлично, наш break point сработал, и все действительно так. Давайте кстати посмотрим, чему у нас равен баланс. Value у нас равно один. Баланс у нас равен 50 000, как мы и хотели. Хорошо. Это у нас третий поток. А теперь давайте посмотрим, например, на второй поток. Чем он занят? И вот здесь очень важно обратить внимание вот на этот вызов. Смотрите, вот у нас вызвался конструктор lock_guard от mutex. Это тот самый объект lock_guard, который мы создали в начале метода spend, и вызывается метод у mutex log, то есть наш поток, он заблокирован на mutex и ждет, когда метод log завершится. Он не может завершиться, потому что другой поток — наш поток номер три, он сейчас находится в критической секции, он держит mutex. Хорошо. Это был второй поток. Третий поток — это наш. Первый поток. Смотрим, что здесь. Здесь ничего такого нет. Значит, это основной поток программы. Есть еще четвертый поток. Давайте посмотрим, чем занят он. И что мы видим? Что он точно так же выполняет функцию spend money, зашел в метод spend и в конструкции lock guard точно так же висит на mutex. То есть он точно так же заблокирован и ждет, когда наш третий поток отпустит mutex. Отлично. Давайте теперь вот что сделаем. Давайте вернемся в наш поток номер три и вот здесь понажимаем resume, чтобы наш break point посрабатывал еще несколько раз. Мы снова попали в третий поток. Я несколько раз понажимал resume. Смотрите, что получилось. Наш break point сработал для четвертого потока. Для четвертого потока. То есть у нас третий поток был в критической секции, он выполнял какие-то изменения, потом он mutex отпустил, и это дало возможность другому потоку, в данном случае четвертому, в критическую секцию войти, захватить mutex и начать в ней работать. То есть четвертый поток у нас в методе spend, а третий поток, вот мы видим, он висит на mutex log. Таким образом, мы видим, что mutex действительно выполняет синхронизацию потоков и действительно защищает критическую секцию от того, чтобы в нее зашло в любой момент больше одного потока. Итак, давайте подведем итоги этого видео. Мы узнали, что для защиты данных от состояния гонки используется класс mutex из одноименного заголовочного файла. Для захвата mutex используется класс lock_guard, который в своем конструкторе mutex захватывает, и освобождает он его в своем деструктуре. Мы на это не посмотрели, но да, это так. То есть lock_guard в конструкторе захватывает mutex, в деструкторе он его освобождает.