c # — Interlocked.CompareExchange & lt; Int & gt; используя GreaterThan или LessThan вместо равенства

System.Threading.Interlocked Объект допускает сложение (вычитание) и сравнение как элементарную операцию. Кажется, что CompareExchange, который просто не выполняет равенство, но также и GreaterThan / LessThan в качестве атомарного сравнения, был бы весьма ценным.

Был бы гипотетический Interlocked.GreaterThan особенность IL или это функция уровня процессора? И то и другое?

Не имея какой-либо другой возможности, возможно ли создать такую ​​функцию в C ++ или прямом коде IL и предоставить эту функциональность C #?

16

Решение

Обновление к последнему посту, который я сделал здесь: мы нашли лучший способ сделать большее сравнение, используя дополнительный объект блокировки. Мы написали много модульных тестов, чтобы убедиться, что блокировка и блокировка могут использоваться вместе, но только в некоторых случаях.

Как работает код: Interlocked использует барьеры памяти, которые для чтения или записи являются атомарными. Синхронизирующая блокировка необходима для того, чтобы сравнение было больше, чем атомарная операция. Итак, теперь правило таково, что внутри этого класса никакая другая операция не записывает значение без этой блокировки синхронизации.

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

Вот код как вид:

Посмотреть здесь: http://files.thekieners.com/blogcontent/2012/ExchangeIfGreaterThan2.png

Здесь как код для копирования&вставить:

public sealed class InterlockedValue
{
private long _myValue;
private readonly object _syncObj = new object();

public long ReadValue()
{
// reading of value (99.9% case in app) will not use lock-object,
// since this is too much overhead in our highly multithreaded app.
return Interlocked.Read(ref _myValue);
}

public bool SetValueIfGreaterThan(long value)
{
// sync Exchange access to _myValue, since a secure greater-than comparisons is needed
lock (_syncObj)
{
// greather than condition
if (value > Interlocked.Read(ref  _myValue))
{
// now we can set value savely to _myValue.
Interlocked.Exchange(ref _myValue, value);
return true;
}
return false;
}
}
}
2

Другие решения

Вы можете строить другие атомные операции из InterlockedCompareExchange.

public static bool InterlockedExchangeIfGreaterThan(ref int location, int comparison, int newValue)
{
int initialValue;
do
{
initialValue = location;
if (initialValue >= comparison) return false;
}
while (System.Threading.Interlocked.CompareExchange(ref location, newValue, initialValue) != initialValue);
return true;
}
38

Что вы думаете об этой реализации:

// this is a Interlocked.ExchangeIfGreaterThan implementation
private static void ExchangeIfGreaterThan(ref long location, long value)
{
// read
long current = Interlocked.Read(ref location);
// compare
while (current < value)
{
// set
var previous = Interlocked.CompareExchange(ref location, value, current);
// if another thread has set a greater value, we can break
// or if previous value is current value, then no other thread has it changed in between
if (previous == current || previous >= value) // note: most commmon case first
break;
// for all other cases, we need another run (read value, compare, set)
current = Interlocked.Read(ref location);
}
}
3

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

Использование выглядит так:

int currentMin = 10; // can be changed from other thread at any moment

int potentialNewMin = 8;
if (InterlockedExtension.AssignIfNewValueSmaller(ref currentMin, potentialNewMin))
{
Console.WriteLine("New minimum: " + potentialNewMin);
}

И вот методы:

public static class InterlockedExtension
{
public static bool AssignIfNewValueSmaller(ref int target, int newValue)
{
int snapshot;
bool stillLess;
do
{
snapshot = target;
stillLess = newValue < snapshot;
} while (stillLess && Interlocked.CompareExchange(ref target, newValue, snapshot) != snapshot);

return stillLess;
}

public static bool AssignIfNewValueBigger(ref int target, int newValue)
{
int snapshot;
bool stillMore;
do
{
snapshot = target;
stillMore = newValue > snapshot;
} while (stillMore && Interlocked.CompareExchange(ref target, newValue, snapshot) != snapshot);

return stillMore;
}
}
3

На самом деле это не так, но полезно думать о параллелизме как о двух формах:

  1. Блокировка свободного параллелизма
  2. Блокировка на основе параллелизма

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

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

Параллелизм на основе блокировок основан на идее «блокировки» доступа к критическому разделу кода. Когда один поток «заблокировал» критическую секцию, никакой другой поток не может иметь код, работающий внутри этой же критической секции. Обычно это делается с помощью «мьютексов», которые взаимодействуют с планировщиком ОС и приводят к тому, что потоки становятся неработоспособными в ожидании входа в заблокированный критический раздел. Другой подход заключается в использовании «спин-блокировок», которые заставляют поток вращаться в цикле, не делая ничего полезного, пока критическая секция не станет доступной.

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

Блокировать свободный параллелизм сложно. Это становится особенно трудно по мере увеличения длины и сложности критических секций. Любой шаг в критической секции может выполняться одновременно любым количеством потоков одновременно, и они могут двигаться с совершенно разными скоростями. Вы должны убедиться, что, несмотря на это, результаты системы в целом остаются правильными. Для чего-то вроде приращения это может быть просто (cs — это всего лишь одна инструкция). Для более сложных критических разделов все может стать очень сложным очень быстро.

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

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

Рассмотрите возможность реализации управляемой кучи. Звонить в ОС каждый раз, когда вызывается «новый», было бы ужасно. Это разрушит производительность вашего приложения. Тем не менее, используя параллельный параллельный доступ без блокировок, можно реализовать распределение памяти поколения 0, используя инкремент с блокировкой (я не знаю наверняка, делает ли это CLR, но я был бы удивлен, если бы это было не так. Это может быть ОГРОМНО экономия.

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

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

В спин-замках обычно используются атомарные инструкции (обычно cas) в их состоянии цикла. Мьютексы должны использовать либо спин-блокировки, либо атомарные обновления внутренних структур ядра в своей реализации.

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

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

Теперь, чтобы ответить на ваш вопрос:

Метод, который выполнял блокированный обмен сравнениями, если он меньше, чем подразумевал бы для вызывающих абонентов, что он не использует блокировки. Вы не можете реализовать это с помощью одной инструкции, так же как можно сделать инкрементный или сравнительный обмен. Вы можете смоделировать это, выполняя вычитание (чтобы вычислить меньше чем), с блокированным обменом сравнения в цикле. Вы также можете сделать это с мьютексом (но это будет означать блокировку, и поэтому использование «interlocked» в названии будет вводить в заблуждение). Уместно ли строить версию «симуляция блокировки через cas»? Это зависит от. Если код вызывается очень часто, и у него очень мало конфликтов с потоками, тогда ответ — да. Если нет, вы можете превратить операцию O (1) с умеренно высокими постоянными коэффициентами в бесконечный (или очень длинный) цикл, и в этом случае было бы лучше использовать мьютекс.

Большую часть времени это того не стоит.

2

Все взаимосвязанные операции имеют прямую поддержку в оборудовании.

Взаимосвязанные операции и атомарный тип данных — это разные вещи. Атомарный тип — это функция уровня библиотеки. На некоторых платформах и для некоторых типов данных атомика реализована с использованием взаимосвязанных инструкций. В этом случае они очень эффективны.

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

Я не уверен, если Interlocked.GreaterThan действительно нужно. В противном случае это может быть уже реализовано. Если вы знаете хороший пример, где это может быть полезно, я уверен, что все здесь будут рады услышать это.

1

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

Нет смысла делать их частью семьи Запертых, поэтому вопрос в том, чего вы на самом деле пытаетесь достичь?

0
По вопросам рекламы [email protected]