Неизменность
Проще говоря, память неизменна, если она не изменена после инициализации.
Программы, написанные на императивных языках, таких как C, Java и C #, могут манипулировать данными в памяти по желанию. Область физической памяти, однажды выделенная, может быть изменена целиком или частично потоком выполнения в любое время во время выполнения программы. Фактически, императивные языки поощряют такой способ программирования.
Написание программ таким способом было невероятно успешным для однопоточных приложений. Однако, поскольку современная разработка приложений движется к нескольким параллельным потокам операций в рамках одного процесса, возникает мир потенциальных проблем и сложности.
Когда существует только один поток выполнения, вы можете себе представить, что этот единственный поток «владеет» всеми данными в памяти и поэтому может манипулировать ими по своему желанию. Тем не менее, не существует неявной концепции владения, когда задействованы несколько выполняющихся потоков.
Вместо этого это бремя ложится на программиста, который должен приложить большие усилия, чтобы гарантировать, что структуры в памяти находятся в согласованном состоянии для всех читателей. Блокирующие конструкции должны использоваться с осторожностью, чтобы запретить одному потоку видеть данные во время их обновления другим потоком. Без этой координации поток неизбежно будет потреблять данные, которые были обновлены только на полпути. Исход из такой ситуации непредсказуем и часто катастрофичен. Более того, выполнение правильной блокировки в коде общеизвестно сложно, а при неправильном выполнении может привести к снижению производительности или, в худшем случае, к блокировкам, которые безвозвратно останавливают выполнение.
Использование неизменяемых структур данных устраняет необходимость введения сложной блокировки в коде. Если гарантируется, что часть памяти не изменится в течение срока действия программы, несколько читателей могут одновременно получить доступ к памяти. Они не могут наблюдать эти конкретные данные в несогласованном состоянии.
Многие функциональные языки программирования, такие как Lisp, Haskell, Erlang, F # и Clojure, поддерживают неизменяемые структуры данных по своей природе. Именно по этой причине они испытывают всплеск интереса, поскольку мы движемся к все более сложной разработке многопоточных приложений и многокомпьютерных компьютерных архитектур.
Государство
Состояние приложения можно просто представить как содержимое всей памяти и регистров ЦП в данный момент времени.
Логически состояние программы можно разделить на две части:
- Состояние кучи
- Состояние стека каждого исполняющего потока
В управляемых средах, таких как C # и Java, один поток не может получить доступ к памяти другого. Следовательно, каждый поток «владеет» состоянием своего стека. Стек можно рассматривать как содержащий локальные переменные и параметры типа значения (struct
), а также ссылки на объекты. Эти значения изолированы от внешних потоков.
Однако данные в куче доступны всем потокам, поэтому необходимо следить за одновременным доступом. Все экземпляры объекта ссылочного типа (class
) хранятся в куче.
В ООП состояние экземпляра класса определяется его полями. Эти поля хранятся в куче и поэтому доступны из всех потоков. Если класс определяет методы, позволяющие изменять поля после завершения работы конструктора, тогда этот класс является изменяемым (не неизменяемым). Если поля не могут быть изменены каким-либо образом, то тип является неизменным. Важно отметить, что класс со всеми полями C # readonly
/ Java final
не обязательно является неизменным. Эти конструкции гарантируют, что ссылка не может измениться, но не объект ссылки. Например, поле может иметь неизменную ссылку на список объектов, но фактическое содержимое списка может быть изменено в любое время.
Определяя тип как действительно неизменяемый, его состояние можно считать замороженным, и поэтому тип безопасен для доступа несколькими потоками.
На практике может быть неудобно определять все ваши типы как неизменяемые. Для изменения значения в неизменяемом типе может потребоваться значительное копирование памяти. Некоторые языки делают этот процесс проще, чем другие, но в любом случае процессор будет выполнять дополнительную работу. Многие факторы влияют на то, перевешивает ли время, затрачиваемое на копирование памяти, влияние блокировок.
Много исследований было уделено разработке неизменных структур данных, таких как списки и деревья. При использовании таких структур, скажем, списка, операция «add» вернет ссылку на новый список с добавленным новым элементом. Ссылки на предыдущий список не видят никаких изменений и по-прежнему имеют согласованное представление данных.