2016-12-02 9 views
31

Joe Albahari имеет great series на многопоточности, который должен быть прочитан и должен быть известен наизусть для всех, кто выполняет многопоточность C#.Является ли слово «volatile» все еще сломанным в C#?

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

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

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

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

Я проверил MSDN documentation, который был последний раз изменялся в 2015 году, но до сих пор списки:

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

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

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

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

+1

В чем смысл внешнего выражения 'return' и присваивания' lock' в вашем примере? – Yarik

+2

Это по-прежнему вводит в заблуждение. Volatile обеспечивает семантику памяти получения/выпуска, которой достаточно для эффективного выполнения многих алгоритмов. Да, это сложно использовать, но это не делает его сломанным (решение C++ явно превосходит, но у них было преимущество видеть проблемы с volatile в java) (И любой, кто считает, что барьеры памяти легче, чем изменчивые, просто не хватает опыт работы с архитектурами, отличными от x86.Попытайтесь использовать барьеры памяти в архитектуре без многократной атомарности и посмотреть, как далеко вы достигнете) – Voo

+0

(Описание msdn, по общему признанию, заметно хуже. Очевидно, кто бы ни писал этот фрагмент, он вообще не понимает изменчивости, и надежда не может быть разрешена только написать одну строку параллельного кода) – Voo

ответ

30

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

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

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

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); 
    } 
} 

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

+2

Вы предполагали, что «a» будет изменчивым или нестабильным на «x», чтобы обеспечить запись «a»? –

+1

@Patrick Код верный как есть. Гарантии упорядочения памяти гораздо более строгие, чем «записи не могут быть кэшированы», и это также играет здесь. Чтобы значительно упростить: если поток B видит обновление для изменчивой переменной X, гарантированно будет видеть все записи, которые произошли ранее в потоке A, когда он написал значение x. Это позволяет использовать один volatile bool для публикации других данных. – Voo

+0

Действительно ли volatile действительно имеет семантику приобретения/выпуска? Или последовательная согласованность? Я бы подумал, что последний, и два не то же самое. – Mehrdad

13

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

+6

Согласен. Я думаю, что акцент должен быть сделан на «Поля, объявленные волатильными, не подлежат ** оптимизации компилятора **, которые предполагают доступ одним потоком, что гарантирует, что самое актуальное значение всегда присутствует в поле. " –

+3

@ MikeSherrill'CatRecall ': «Компилятор» - это красная селедка. Безопасность потоков - это проблема, которая выходит далеко за рамки компилятора. Например, переупорядочивание в ЦП также плохое. – Mehrdad

+2

Каждый компилятор должен понимать правила переупорядочения ЦП. Если инструкции A и B могут быть переупорядочены CPU, но языковая семантика диктует иначе, тогда компилятор ** должен ** ввести некоторый тип ограждения, который предотвращает переупорядочение, или выбирает альтернативные инструкции для достижения той же цели. Следовательно, когда 'volatile' семантика prohinit переупорядочивания, это до компилятора, чтобы не просто не переупорядочивать себя, но и предотвратить CPU от этого. – MSalters

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 вокруг всего атомного блока, иначе планирование все равно будет приводить к недетерминированному поведению.

+1

«Это в основном означает, что переменная не кэшируется» ... вздох и еще один человек, увековечивающий этот миф :(Нет, это абсолютно не то, что нестабильные гарантии, а на самом деле неустойчивые поля отлично кэшируются на x86 и других архитектурах. Также Volatile абсолютно запрещает переупорядочивать обращения к памяти, что необходимо для его полезности. Даже если бы было разумное определение «не кэшировано» (нет), это абсолютно бесполезно для абсолютно каждого алгоритма. – Voo

+0

@ Voo Просто говорю, чему меня учили, извините. Правильнее ли это: «Это означает, что переменная не подвержена оптимизации компилятора, которые предполагают доступ из одного потока. [...] Без летучих, один многопроцессорный компьютер без изменчивости , компилятор может делать предположения о однопоточном доступе, например, сохраняя значение в регистре, что предотвращает доступ других процессоров к последнему значению ». – zstewart

+1

Это правильно, но не хватает необходимых деталей того, что происходит волатильность. Volatile гарантирует получение/выпуск семантики при чтении и записи. Например, вы не можете изменить порядок записи * после * волатильной записи (вы можете изменить порядок, прежде чем волатильная запись будет прекрасной, хотя). Если вы хотите начать понимать все это, вы можете, например, начать с [этого] (https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/). Речь идет о JMM, но CLR MM довольно похожа на практике. – Voo