Неустойчивые поля в С#

Из спецификации 10.5.3 Летучие поля:


Тип энергозависимого поля должен быть одним из следующих:

  • Тип ссылки.

  • Тип байта, sbyte, short, ushort, int, uint, char, float, bool, System.IntPtr или System.UIntPtr.

  • Тип перечисления, имеющий базовый тип перечисления байта, sbyte, short, ushort, int, или uint.


Сначала я хочу подтвердить, что мое понимание верное: я думаю, что вышеупомянутые типы могут быть изменчивыми, поскольку они хранятся в виде 4-байтового блока в памяти (для ссылочных типов из-за его адреса), что гарантирует операцию чтения/записи является атомарным. Тип double/long/etc не может быть изменчивым, поскольку они не являются атомарным чтением/записью, так как они содержат более 4 байтов в памяти. Правильно ли я понимаю?

И второе, если первое предположение верно, почему пользовательская структура с единственным полем int в нем (или что-то подобное, 4 байта в порядке) не может быть изменчивым? Теоретически это атомное право? Или это не допускается просто потому, что все пользовательские структуры (возможно, более 4 байтов) не разрешены к летучим по дизайну?

Ответ 1

В принципе, использование ключевого слова volatile иногда может вводить в заблуждение. Его цель - гарантировать, что последнее значение аннотированного элемента возвращается при доступе к любому потоку.

Собственно, это верно для типов значений только 1. Элементы ссылочного типа являются действительными указателями на местоположение в куче, где хранится объект. Таким образом, при использовании ссылочного типа volatile гарантирует, что вы получите самую последнюю ссылку (указатель) на объект, а не на сам объект.

Итак, если у вас есть volatile List<String> myVolatileList, который изменен несколькими потоками (добавлены или удалены элементы), и если вы ожидаете безопасного доступа к последней модификации списка, вы на самом деле неправильно. На самом деле, вы подвержены тем же проблемам, что и ключевое слово volatile, но в этом случае вам это не поможет, и он не обеспечивает безопасность потоков.

Если, однако, сам список не изменяется разными потоками, но, скорее, каждый поток будет присваивать другой экземпляр для поля (это означает, что список ведет себя как неизменяемый объект) тогда вы в порядке. Вот пример:

public class HasVolatileReferenceType
{
    public volatile List<int> MyVolatileMember;
}

Следующее использование верно в отношении многопоточности, поскольку каждый поток заменяет указатель MyVolatileMember. Здесь volatile гарантирует, что другие потоки будут видеть последний экземпляр списка, хранящийся в поле MyVolatileMember.

HasVolatileReferenceTypeexample = new HasVolatileReferenceType();
// instead of modifying `example.MyVolatileMember`
// we are replacing it with a new list. This is OK with volatile.
example.MyVolatileMember = example.MyVolatileMember
     .Where(x => x > 42).ToList();

Напротив, приведенный ниже код подвержен ошибкам, поскольку он напрямую изменяет список. Если этот код выполняется одновременно с несколькими потоками, список может быть поврежден или вести себя несогласованно.

example.MyVolatileMember.RemoveAll(x => x <= 42);

Вернемся к значениям значений в течение некоторого времени. В .NET все типы значений на самом деле переназначены, когда они изменены, они безопасны для использования с ключевым словом volatile - см. Код:

public class HasVolatileValueType
{
    public volatile int MyVolatileMember;
}

// usage
HasVolatileValueType example = new HasVolatileValueType();
example.MyVolatileMember = 42;

1 В общем случае volatile подходит для любого неизменяемого объекта, так как модификации всегда подразумевают переназначение поля с другим значением. Следующий код также является правильным примером использования ключевого слова volatile:

public class HasVolatileImmutableType
{
    public volatile string MyVolatileMember; 
}

// usage
HasVolatileImmutableType example = new HasVolatileImmutableType();
example.MyVolatileMember = "immutable";
// string is reference type, but is *immutable*, 
// so we need to reasign the modification result it in order 
// to work with the new value later
example.MyVolatileMember = example.MyVolatileMember.SubString(2);

Я бы рекомендовал вам взглянуть на в этой статье. Он полностью объясняет использование ключевого слова volatile, способ его работы и возможные последствия его использования.

Ответ 2

Я думаю, что это потому, что struct - это тип значения, который не является одним из типов, перечисленных в спецификациях. Интересно отметить, что ссылочные типы могут быть нестабильным полем. Таким образом, это может быть выполнено с помощью определяемого пользователем класса. Это может опровергнуть вашу теорию о том, что вышеупомянутые типы являются летучими, поскольку они могут храниться в 4 байтах (или, может быть, нет).

Ответ 3

Это обоснованное предположение в ответ... пожалуйста, не стреляйте в меня слишком сильно, если я ошибаюсь!

документация для изменчивых:

Модификатор volatile обычно используется для поля, к которому обращаются несколько потоков, без использования оператора блокировки для сериализации доступа.

Это подразумевает, что часть намерения дизайна для изменчивых полей заключается в реализации многопоточного доступа без блокировки.

Член структуры может обновляться независимо от других членов. Поэтому, чтобы написать новое значение структуры, где только часть его была изменена, необходимо прочитать старое значение. Поэтому для записи не требуется отдельная операция с памятью. Это означает, что для надежного обновления структуры в многопоточной среде требуется некоторая блокировка или другая синхронизация потоков. Обновление нескольких элементов из нескольких потоков без синхронизации может вскоре привести к контр-интуитивным, если не технически коррумпированным результатам: сделать struct volatile будет означать неатомный объект, который будет обновляться с помощью атома.

Кроме того, только некоторые структуры могут быть энергозависимыми - размером 4 байта. Код, определяющий размер - определение структуры, может находиться в полностью отдельной части программы до той, которая определяет поле как изменчивое. Это может ввести в заблуждение, так как будут непреднамеренные последствия обновления определения структуры.

Итак, хотя было бы технически возможно позволить некоторым структурам быть изменчивыми, оговорки для правильного использования были бы достаточно сложными, чтобы недостатки перевешивали преимущества.

Моя рекомендация об обходном пути состояла в том, чтобы сохранить вашу 4-байтовую структуру как 4-байтовый базовый тип и реализовать статические методы преобразования, чтобы использовать каждый раз, когда вы хотите использовать это поле.

Ответ 4

Чтобы решить вторую часть вашего вопроса, я бы поддержал решение дизайнеров языков на основе двух пунктов:

KISS - Keep It Simple Simon - это сделало бы сложнее и сложнее использовать эту спецификацию. Все языковые функции начинаются с минус 100 пунктов, добавляет способность иметь небольшое меньшинство неустойчивых стэтов, на самом деле стоит 101 балл?

Совместимость - вопросы о сериализации в стороне - Обычно добавление нового поля в тип [class, struct] является безопасным обратным движением, совместимым с исходным кодом. Если вы добавляете поле, это не должно прерывать компиляцию anyones. Если поведение структур изменилось при добавлении поля, это нарушило бы это.