Решение 11 распространенных проблем в многопоточном коде - Неизменность
ОГЛАВЛЕНИЕ
Неизменность
Неизменная структура данных – это структура, не изменяющаяся после создания. Это замечательное свойство для одновременных программ, поскольку если данные не изменяются, то нет и риска конфликта при одновременном доступе многих потоков. Это значит, что не нужно думать о синхронизации.
Неизменность в некоторой степени поддерживается в C++ через const, а в C# через модификатор «только для чтения». Например, тип .NET, у которого существуют лишь поля только для чтения, является неглубоким неизменным. F# создает неглубокие неизменные типы по умолчанию, если не использовать модификатор открытости для изменений (mutable). Заходя на шаг дальше, если каждое из этих полей само ссылается на другой тип, все поля которого предназначены только для чтения (и ссылается на глубоко неизменные типы), то тип является глубоко неизменным. Результатом является законченная объектная структура, которая гарантированно не изменится когда не надо, что очень полезно.
Все это описывает неизменность как статическое свойство. Объекты также могут быть неизменными по соглашению, что означает наличие некоей гарантии неизменности состояния в течение определенных периодов времени. Это динамическое свойство. Cвойство freezable (фиксируемое) Windows Presentation Foundation (WPF) реализует именно это, и оно также делает возможным параллельный доступ без синхронизации (хотя его и нельзя проверить так же, как статическую поддержку). Динамическая неизменность часто полезна для объектов, которые превращаются из неизменных в открытые для изменений в ходе своего жизненного цикла.
У неизменности есть некоторые недостатки. Например, когда что-либо должно измениться, необходимо будет создать копию первоначального объекта и применить изменения по пути. Кроме того, в графе объектов обычно невозможны циклы (за исключением динамической неизменности).
Например, представьте себе, что у вас есть ImmutableStack<T>, как показано на рис. 4. Вместо набора изменяющихся методов Push («Принудительная отправка») и Pop («Извлечение») из них потребуется вернуть новые объекты ImmutableStack<T>, которые содержат примененные изменения. В некоторых случаях можно использовать хитрые трюки (как в случае стека), чтобы экземпляры использовали общую память.
Рис. 4. Использование ImmutableStack<T>
public class ImmutableStack<T>
{
private readonly T m_value;
private readonly ImmutableStack<T> m_next;
private readonly bool m_empty;
public ImmutableStack()
{
m_empty = true;
}
internal ImmutableStack(T value, Node next)
{
m_value = value;
m_next = next;
m_empty = false;
}
public ImmutableStack<T> Push(T value)
{
return new ImmutableStack(value, this);
}
public ImmutableStack<T> Pop(out T value)
{
if (m_empty)
throw new Exception("Empty.");
return m_next;
}
}
По мере проталкивания узлов в стек каждому из них необходимо выделить объект. В стандартной реализации стека при помощи связанного списка это пришлось бы сделать в любом случае. Но обратите внимание на то, что при извлечении элементов из стека можно использовать существующие объекты. Это обусловлено тем, что каждый узел в стеке неизменен.
Некоторые неизменные типы уже выпущены на волю. Класс CLR System.String неизменен, и существует проектная рекомендация, согласно которой все новые типы значений также должны быть неизменными. Здесь же я рекомендую использовать неизменность, когда это возможно и кажется естественным, сопротивляясь соблазну выполнения изменений просто потому, что это удобно в нынешнем поколении языков.