2013-07-22 2 views
4

Для нижеприведенного случая, когда нет конкуренции за записи между рабочими потоками, требуется ли блокировка или неустойчивость? Любая разница в ответе, если доступ «Peek» не требуется в «G».Требуется блокировка или энергозависимость, когда рабочие потоки неконкурентоспособно записываются в локальные переменные или переменные класса?

class A 
{ 
    Object _o; // need volatile (position A)? 
    Int _i; // need volatile (position B)? 

    Method() 
    { 
     Object o; 
     Int i; 

     Task [] task = new Task[2] 
     { 
     Task.Factory.StartNew(() => { 
       _o = f1(); // use lock() (position C)? 
       o = f2(); // use lock() (position D)? 
     } 
     Task.Factory.StartNew(() => { 
       _i = g1(); // use lock() (position E)? 
       i = g2(); // use lock() (position F)? 
     }   
     } 

     // "Peek" at _o, _i, o, i (position G)? 

     Task.WaitAll(tasks); 

     // Use _o, _i, o, i (position H)? 
} 
+1

Ответ Сервита правильный, но будьте осторожны с вашими ожиданиями в позиции G, так как вы можете получить некоторые неинтуитивные результаты. Например, вы можете 'o' назначить новый объект, но' _o' все еще является нулевым, несмотря на то, что они должны были произойти в обратном порядке. – Douglas

+0

Кроме того, если целью вашего «заглядания» является обработка результатов завершенной задачи до того, как все остальные также будут завершены, тогда правильный путь - заменить ваш 'WaitAll' на цикл вызовов' WaitAny' , – Douglas

ответ

5

Запись в ссылочных типов (т.е. Object) и типов слов размера значений (т.е. int в битовой системе 32) являются атомарными. Это означает, что когда вы заглядываете в значения (позиция 6), вы можете быть уверены, что либо получите старое значение, либо новое значение, но не что-то другое (если у вас есть такой тип, как большая структура, которую он может сращивать, и вы можете прочитать значение, когда оно было на полпути, написанное). Вам не нужно a lock или volatile, если вы готовы принять потенциальный риск чтения устаревших значений.

Обратите внимание, что поскольку в этот момент не введен барьер памяти (lock или использование volatile, добавьте один), возможно, что переменная была обновлена ​​в другом потоке, но текущий поток не соблюдает это изменение ; он может читать «устаревшее» значение (потенциально) через некоторое время после его изменения в другом потоке. Использование volatile гарантирует, что текущий поток может наблюдать изменения в переменной раньше.

Вы можете быть уверены, что после звонка на номер WaitAll вы получите соответствующее значение, даже без lock или volatile.

Также обратите внимание, что, хотя вы можете быть уверены, что ссылка на ссылочный тип написана атомарно, ваша программа не дает никаких гарантий относительно наблюдаемого порядка любых изменений фактического объекта, на который ссылается ссылка. Даже если с точки зрения фонового потока объект инициализируется до того, как он будет назначен полю экземпляра, это может не произойти в этом порядке. Таким образом, другой поток может наблюдать запись ссылочного объекта, но затем следовать этой ссылке и находить объект в инициализационном или частично инициализированном состоянии. Введение барьера памяти (т. Е. С помощью переменной volatile может потенциально позволить вам избежать выполнения таких переупорядочений, тем самым гарантируя, что этого не произойдет. Вот почему это better to just not do this in the first place и просто вернуть две задачи результаты, которые они генерируют, а не манипулируют закрытой переменной.

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

+0

@EricLippert: Я не согласен. Атомарность записи - это то, что могло бы иметь значение, если рассматривать 'int' vs' long' назначения на 32-битной машине. – Douglas

+0

@EricLippert В позиции G (6 в предыдущей редакции) в дополнение к задаче не выполняется никаких барьеров памяти. По-прежнему возможно выбрать один из двух значений, даже без барьера памяти. После 'WaitAll' вы можете быть уверены, что все чтения приведут к правильным значениям *, потому что * он вводит барьер памяти. Учитывая это, вы могли бы утверждать, что ответ недостаточно объяснен (так как я не объясняю, почему мое последнее предложение истинно), но я не вижу, что об этом * неверно *? – Servy

+0

@ Дуглас: То, что вы не согласны, не меняет факты. Здесь важно, чтобы процессор и джиттер не были допущены к оптимизации, которые заставили бы устаревшие значения считываться из переменных. Атомное считывание неправильного значения по-прежнему считается неправильным значением. –

1

В позиции G вы можете наблюдать значения _o и _i могут сохранять свои инициализированные значения null и 0 соответственно, или они могут содержать значения, записанные задачами. На этой позиции это непредсказуемо.

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

Таким образом, в этом конкретном примере явный блокиратор блокировки или памяти (volatile) технически не требуется.

7

Безопасная вещь - не делать этого в первую очередь. Не записывайте значение в один поток и сначала читайте значение в другом потоке.Сделайте Task<object> и Task<int>, которые возвращают значения в поток, который им нужен, вместо выполнения задач, которые изменяют переменные в потоках.

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

Как замечает Брайан Гидеон в своем ответе, вы получаете барьер памяти от WaitAll, но я не помню, если это задокументированная гарантия или только деталь реализации.

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

+0

Я не думаю, что его документально подтвердил (по крайней мере, не определенно). Это просто мое * предположение *, хотя почти со 100% уверенностью, что оно действительно создает барьер памяти. То, что вы делаете, по-прежнему остается тем же ... просто не начинайте так! –

+0

[Albahari] (http://www.albahari.com/threading/part4.aspx#_Full_fences) утверждает, что он всегда генерируется при ожидании «Задачи» (точно так же, как при вводе/выходе «блокировки»). Я не знаю, насколько авторитетна эта книга, хотя я нашел ее одной из лучших ссылок на эту тему. – Douglas

+1

@Douglas: Я был техническим обозревателем самого последнего выпуска C# в двух словах, и это было тяжелой работой *, позвольте мне рассказать вам. Альбахарис знает свои вещи. –

Смежные вопросы