2009-09-09 3 views
18

Я полностью понимаю атомарность, которую предоставляет Threading.Interlocked класс; Однако я не понимаю, почему функция Add предлагает только две перегрузки: одну для целых, другую для Longs. Почему бы не удвоить или какой-либо другой цифровой тип?Почему нет перегрузки Interlocked.Add, который принимает пары в качестве параметров?

Очевидно, что предполагаемый способ изменения Double - CompareExchange; Я УГАДАЮ это, потому что изменение Double - более сложная операция, чем изменение целого. Тем не менее мне непонятно, почему, если CompareExchange и Add могут принимать целые числа, они не могут также принимать парные разряды.

ответ

24

Класс блокировки обертывается вокруг функций Windows с включенными функциями **.

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

BT, BTS, BTR, BTC, XCHG, XADD, ADD, OR, АЦП, SBB, И, SUB, XOR, NOT, NEG, INC, декабрь

Вы заметите, что это, в свою очередь, в значительной степени относится к взаимосвязанным методам. К сожалению, функции ADD для нецелочисленных типов здесь не поддерживаются. Добавление для 64-битных длин поддерживается на 64-битных платформах.

Вот отличная статья discussing lock semantics on the instruction level.

+2

Ссылка не активна. Вот резервная копия из интернет-архива. Https://web.archive.org/web/20160319061137/http://www.codemaestro.com/reviews/8 –

-1

Как Адам Робинсон отметил, есть перегрузка для Interlocked.Increment, который принимает Int64, но примечание:

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

1

Я подозреваю, что есть две причины.

  1. Процессоры, ориентированные на .Net, поддерживают блокировку приращения только для целых типов. Я считаю, что это префикс LOCK на x86, вероятно, подобные инструкции существуют для других процессоров.
  2. Добавление одного из чисел с плавающей запятой может привести к тому же числу, если оно достаточно велико, поэтому я не уверен, что вы можете назвать это приращением. Возможно, разработчики фреймворка стараются избегать неинтуитивного поведения в этом случае.
+0

Что касается вашего второго пункта: да, но я спрашиваю о Interlocked.Add, а не Interlocked.Increment. –

7

Как сказал Рид Копси, карта операций с блокировкой (через функции Windows API) с инструкциями, поддерживаемыми непосредственно процессорами x86/x64. Учитывая, что одной из этих функций является XCHG, вы можете выполнять атомную операцию XCHG, не заботясь о том, что представляют собой биты в целевом местоположении. Другими словами, код может «притворяться», что 64-разрядный номер с плавающей запятой, который вы обмениваете, на самом деле является 64-битным целым числом, и инструкция XCHG не будет знать разницу. Таким образом, .Net может обеспечить блокировку.Функции обмена для поплавков и удваиваются путем «притворяясь», что они являются целыми и длинными целыми числами соответственно.

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

21

Другие имеют обратился к «почему?». Легко, однако, свернуть свой собственный Add(ref double, double), используя CompareExchange примитивно:

public static double Add(ref double location1, double value) 
{ 
    double newCurrentValue = location1; // non-volatile read, so may be stale 
    while (true) 
    { 
     double currentValue = newCurrentValue; 
     double newValue = currentValue + value; 
     newCurrentValue = Interlocked.CompareExchange(ref location1, newValue, currentValue); 
     if (newCurrentValue == currentValue) 
      return newValue; 
    } 
} 

CompareExchange устанавливает значение location1 быть newValue, если текущее значение равно currentValue. Так как это делает атомным, потокобезопасным способом, мы можем полагаться только на него, не прибегая к блокировкам.

Почему цикл while (true)? Подобные циклы являются стандартными при реализации оптимистически параллельных алгоритмов. CompareExchange не изменится location1, если текущее значение отличается от currentValue. I был инициализирован currentValue - location1 - выполнение долговременного чтения (которое может быть устаревшим, но это не меняет правильности, так как CompareExchange проверит значение). Если текущее значение (неподвижное) - это то, что мы читали от location, CompareExchange изменит значение на newValue. Если нет, мы должны повторить CompareExchange с новым текущим значением, которое возвращается CompareExchange.

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

+0

Можете ли вы еще немного объяснить, как это работает? Почему вы петляете? И какова роль CompareExchange? – Mzn

+1

@Mzn Добавлено в мой ответ. Надеюсь, что это объяснит. –

+2

Это отличный ответ! По крайней мере, сейчас я думаю, что знаю, что такое оптимистичный параллелизм: «Написание параллельного кода с предположением, что все будет в порядке». В случае нашего while (true) это означает, что мы не будем зацикливаться навсегда (мы достаточно оптимистичны). Большое спасибо! – Mzn

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