Ответ 1
Неизменяемые значения имеют несколько приложений, для которых они хорошо подходят. Одновременная/параллельная обработка - это лишь одна из них, которая в последнее время приобрела более важное значение. Следующее - действительно самый простой дайджест из опыта и многих книг и разговоров о предмете. Возможно, вам придется немного погрузиться в некоторых.
Основной пример, который вы здесь показываете, - это управление глобальным состоянием, поэтому его нельзя выполнять исключительно "неизменно". Однако даже здесь есть очень веские причины использовать неизменяемые структуры данных. Некоторые из них с головы:
- try-catch ведет себя намного лучше, потому что вы не изменяете общий объект, который может быть изменен на полпути, с неизменяемыми значениями, он автоматически сохраняет последнее согласованное состояние
- сокращение изменяющегося состояния до нескольких многоквартирных безопасных операций "сравнения и свопинга" на очень ограниченном наборе глобальных переменных (в идеале один), полностью устраняет взаимоблокировки
- свободный проход структур данных без какого-либо оборонительного копирования, что довольно часто случается с таинственными ошибками при забытии (много раз защитные копии создаются как для вызывающих, так и для вызываемых функций, потому что разработчики начинают склоняться к "лучше безопасно, чем извиняться" после пара сеансов отладки)
- намного проще модульное тестирование, поскольку многие функции, работающие с неизменяемыми значениями, свободны от побочных эффектов.
- обычно проще сериализация и более прозрачная семантика сравнения гораздо проще отлаживать и принимать (регистрируя) текущий снимок состояния системы даже асинхронно
Вернемся к вашему вопросу.
В самом тривиальном случае глобальное состояние в этом случае часто моделируется с использованием одной изменчивой ссылки в верхнем холдинге на неизменную структуру данных.
Ссылка обновляется только с помощью атомной операции CAS.
Неизменяемая структура данных преобразуется свободными функциями побочного эффекта, и когда все преобразования выполняются, эта ссылка обменивается атомарно.
Если два потока/ядра хотят сменять одновременно новые значения, полученные из того же старого, тот, кто делает это первым, выигрывает другой, не удался (семантика CAS) и ему нужно повторить операцию (в зависимости от преобразования, либо обновления текущий с новым значением или преобразование нового значения с самого начала). Это может показаться расточительным, но предположение здесь состоит в том, что повторная работа некоторых работ зачастую дешевле, чем постоянная блокировка/синхронизация.
Конечно, это может быть оптимизировано, например. путем разделения независимых частей неизменяемых структур данных для дальнейшего уменьшения потенциальных столкновений путем одновременного обновления нескольких ссылок.
Доступ к структуре данных блокируется и очень быстро и всегда дает последовательный ответ. Крайные случаи, например, когда вы отправляете обновление, а другой клиент получает более старые данные после этого, следует ожидать в любой системе, так как сетевые запросы могут выйти из строя тоже...
STM полезен довольно редко, и обычно вам лучше использовать атомные свопы структуры данных, содержащие все значения из ссылок, которые вы использовали бы в транзакции STM.