Мы с вами говорили, что в многопоточных программах нет необходимости выполнять синхронизацию доступа к константным объектам. Ну, потому что мы можем вызывать только константные методы и не можем изменить объект. Поэтому нет необходимости синхронизировать доступ. Однако в предыдущем видео мы с вами узнали о константности кое-что новое. А именно, мы познакомились с mutable полями. И поэтому нам нужно еще раз посмотреть на константность многопоточных программ. Давайте воспользуемся шаблоном LazyValue, который вы разработали в задаче перед этим видео. И воспользуемся мы им таким образом: мы создадим ленивый map из строки в число, который в случае обращения будет инициализироваться списком населения городов России. То есть вот у нас здесь объявлен инициализатор для нашего LazyValue, и здесь перечислены города России и их население по состоянию на 2018 год. Более того, мы к нашему объекту city_population будем обращаться из разных потоков. Вот я здесь написал простую функцию, которая будет обращаться к city_population, вызывать у него метод get и узнавать, а сколько же людей живет в городе Тула. Давайте посмотрим на код, который идет дальше. Мы порождаем с помощью функции async 25 потоков, каждый из которых будет выполнять вот эту нашу функцию — kernel. И дальше мы дожидаемся, когда все потоки отработают, и в конце программы еще выводим население города Саратов. Давайте посмотрим внутрь функции kernel. Она вызывает метод get у объекта city_population. Метод get — это метод нашего шаблона LazyValue, этот метод константный, и поэтому, как мы говорили, нам нет необходимости выполнять какую-либо синхронизацию доступа к объекту city_population, потому что мы у него вызываем только константные методы. Ну, хорошо. Вроде программа написана правильно. Давайте мы ее скомпилируем и запустим. Мы ее запускаем, она у нас корректно отработала и вывела, что в Саратове живет 836 900 человек. Однако давайте мы позапускаем нашу программу несколько раз. Вот она снова корректно отработала. Продолжаем запускать. И она пока что продолжает корректно работать. Ага. А вот мы сейчас ее запустили, и она что-то подвисла. Она подвисла и что-то думает, думает... и она завершилась, но в консоли ничего не выведено. Кроме того, если мы посмотрим вот сюда, то нам Eclipse пишет, что код возврата нашего процесса — минус 1 миллиард 73 миллиона и так далее. Ну, то есть наша программа упала. Она отработала некорректно и вернула ненулевой код возврата. Значит, с ней что-то не то. И так как мы сделали достаточно много успешных запусков, то мы можем сразу предположить, что дело у нас явно в многопоточной коммуникации, и явно у нас наши потоки обращаются к объекту city_population, и иногда это не получается. Давайте заглянем внутрь метода get, и, думаю, тут вам станет очевидна причина падения нашей программы. Смотрите: мы проверяем, что, если поле value типа optional непроинициализировано, не хранит никакого значения, то мы заходим внутрь условного оператора и инициализируем поле value. Ну а теперь давайте представим: у нас несколько потоков параллельно приходят в метод get, одновременно смотрят на поле value, видят, что оно пустое, оно не хранит никакого значения, заходят внутрь, параллельно выполняют функцию init и потом параллельно начинают записывать в поле value целый map. Ну и так как map — это сложный объект, это дерево поиска, при параллельной записи что-то пошло не так. Что-то иногда идет не так. Что нам нужно сделать, чтобы таких ситуаций не возникало, а мы имеем дело с классической ситуацией гонки, нам нужно выполнить синхронизацию. Синхронизацию доступа к полю value. Как это делается? С помощью мьютекса. Добавляем мьютекс в наш объект, и вот здесь делаем так. Объявляем lock guard g, инициализируем его нашим мьютексом в условном операторе. Таким образом, перед тем как мы будем читать поле value, мы захватим мьютекс и гарантируем, что один, ровно один поток, сначала прочитает поле value, а потом выполнит его инициализацию. Давайте скомпилируем наш код, и он не компилируется. Странно. Почему? Давайте посмотрим на сообщение компилятора. Компилятор пишет: passing std lock guard mutex type aka const std mutex as this argument discards qualifiers. Очень знакомое нам сообщение, которое говорит о том, что у нас что-то не то с константностью. При этом мы тут видим, что мы передаем объект const mutex. Ну, логично. У нас метод get константный, а мы здесь, создавая lock guard, пытаемся mutex захватить. То есть изменить его. А mutex у нас не объявлен со словом mutable, поэтому изменять его нельзя. Что надо сделать? Нужно объявить mutex с ключевым словом mutable. Тогда наш код будет компилироваться. Он компилируется. Мы его запустим, и он корректно работает. Если его позапускать много раз, он все равно будет работать корректно, потому что мы сделали здесь синхронизацию. На самом деле, в реальных программах, вот в такой ситуации, когда у нас есть какой-то многопоточный кэш, доступ к нему реализуют немного по-другому, потому что даже когда наш кэш, вот это value, оно проинициализировано, мы все равно при каждом обращении захватываем mutex. Это, безусловно, не очень эффективно. Поэтому в реальных программах это делают чуть иначе, но для нашего учебного примера мы взяли простой вариант. Что мы хотели показать этим примером? То, что, если вы разрабатываете класс, который предназначен для использования в многопоточных программах, то его mutable поля должны быть потокобезопасными. Потому что вы изменяете эти поля внутри константных методов. И поэтому, если вы не гарантируете в mutable полях потокобезопасность, программа может вести себя некорректно. Еще одна важная вещь, которую мы с вами узнали, состоит в том, что поля типа mutex, можно сказать, хотят быть mutable. Потому что сами по себе они потокобезопасны — это же примитив синхронизации, и он точно не является частью наблюдаемого состояния. И поэтому нет смысла делать мьютексы не mutable, потому что они чаще всего захватываются внутри константных методов только для того, чтобы гарантировать потокобезопасность. Ну и я еще раз напомню, что в C++ предполагается, что все константные методы являются потокобезопасными. Собственно, именно поэтому и нужно делать mutable поля потокобезопасными, чтобы гарантировать, что в многопоточной программе при обращении к константным объектам нет необходимости выполнять внешнюю синхронизацию доступа.