Подтвердить что ты не робот

Является ли слово "volatile" еще сломанным в С#?

Joe Albahari имеет отличную серию для многопоточности, которая должна быть прочитана и должна быть известна наизусть для любого, кто выполняет многопоточность С#.

В части 4, однако, он упоминает проблемы с изменчивой:

Обратите внимание, что применение volatile не предотвращает запись, за которой следует читать от обмена, и это может создать головоломки. Джо Даффи хорошо иллюстрирует проблему в следующем примере: если Test1 и Test2 запускается одновременно на разных потоках, его возможно для a и b для обоих в конечном итоге значение 0 (несмотря на использование volatile on и x и y)

Далее следует примечание о том, что документация MSDN неверна:

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

Я проверил документацию MSDN, которая была в последний раз изменена в 2015 году, но по-прежнему перечисляет:

Ключевое слово volatile указывает, что поле может быть изменено несколько потоков, которые выполняются в одно и то же время. Поля, которые объявленные volatile не подпадают под оптимизацию компилятора, что допускать доступ одним потоком. Это гарантирует, что большинство текущее значение всегда присутствует в поле.

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

private int foo;
private object fooLock = new object();
public int Foo {
    get { lock(fooLock) return foo; }
    set { lock(fooLock) foo = value; }
}

Поскольку части о многопоточности были написаны в 2011 году, аргумент все еще действует сегодня? Следует ли избегать волатильности во что бы то ни стало в пользу блокировок или полных ограждений памяти, чтобы предотвратить очень сложное создание ошибок, которые, как уже упоминалось, даже зависят от поставщика ЦП, на котором он работает?

4b9b3361

Ответ 1

Летучие в своей текущей реализации не нарушаются, несмотря на то, что в популярных блогах, претендующих на такую ​​вещь. Это, однако, плохо указано, и идея использования модификатора в поле для указания порядка памяти не так велика (сравните volatile в Java/С# с атомарной спецификацией С++, у которой было достаточно времени, чтобы учиться на более ранних ошибках). С другой стороны, статья MSDN была написана кем-то, у которого нет деловых разговоров о concurrency, и это полностью фиктивный. Единственный разумный вариант - полностью игнорировать его.

Volatile гарантирует получение/выпуск семантики при доступе к полю и может применяться только к типам, которые позволяют читать и записывать атомы . Не больше, не меньше. Этого достаточно для эффективного использования многих алгоритмов блокировки, таких как неблокирующие хэшмапы.

Один очень простой пример - использование изменчивой переменной для публикации данных. Благодаря неустойчивому по x, утверждение в следующем фрагменте не может срабатывать:

private int a;
private volatile bool x;

public void Publish()
{
    a = 1;
    x = true;
}

public void Read()
{
    if (x)
    {
        // if we observe x == true, we will always see the preceding write to a
        Debug.Assert(a == 1); 
    }
}

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

Ответ 2

Когда я прочитал документацию MSDN, я полагаю, что он говорит, что если вы видите volatile для переменной, вам не нужно беспокоиться о оптимизации компилятора, закручивая значение, потому что они изменяют порядок операций. Он не говорит, что вы защищены от ошибок, вызванных тем, что ваш собственный код выполняет операции над отдельными потоками в неправильном порядке. (хотя, по общему признанию, комментарий не ясен относительно этого.)

Ответ 3

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

Как показывает пример кода, который вы упомянули, он не защищает вас от необходимости использования методов в разных блоках. В действительности volatile делает каждый отдельный доступ к переменной volatile переменной атомарной. Он не дает никаких гарантий относительно атомарности групп таких доступов.

Если вы просто хотите убедиться, что ваше свойство имеет последнее единственное значение, вы должны просто использовать volatile.

Проблема возникает, если вы пытаетесь выполнить несколько параллельных операций, как если бы они были атомарными. Если вам нужно заставить несколько операций быть атомарными вместе, вам нужно заблокировать всю операцию. Рассмотрим пример снова, но используя блокировки:

class DoLocksReallySaveYouHere
{
  int x, y;
  object xlock = new object(), ylock = new object();

  void Test1()        // Executed on one thread
  {
    lock(xlock) {x = 1;}
    lock(ylock) {int a = y;}
    ...
  }

  void Test2()        // Executed on another thread
  {
    lock(ylock) {y = 1;}
    lock(xlock) {int b = x;}
    ...
  }
}

Причины блокировки могут вызвать некоторую синхронизацию, что может помешать как a, так и b иметь значение 0 (я не тестировал это). Однако, поскольку оба x и y заблокированы независимо, либо a, либо b могут все еще не детерминироваться в конечном итоге со значением 0.

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