Считать актуальное значение из переменной Interlocked с единственной записью в переменную
Я хотел бы создать класс с двумя методами:
void SetValue(T value)
сохраняет значение, но позволяет хранить только одно значение (в противном случае создается исключение).T GetValue()
извлекает значение (и выдает исключение, если значения еще нет).
У меня есть следующие желания / ограничения:
- Чтение значения должно быть дешевым.
- Запись значения может быть (умеренно) дорогостоящей.
GetValue()
должен генерировать исключение, только если актуальное значение отсутствует (null
): Он не должен выдавать исключение, основанное на устаревшемnull
значение после звонкаSetValue()
в другой теме.- Значение записывается только один раз. Это означает
GetValue()
не нужно обновлять значение, если оно не равно нулю. - Если можно избежать полного барьера памяти, то это (намного) лучше.
- Я понимаю, что параллелизм без блокировок лучше, но я не уверен, что это так.
Я придумал несколько способов добиться этого, но я не уверен, какие из них правильные, какие эффективны, почему они (не) правильны и (не) эффективны, и есть ли лучший способ добиться того, чего я хочу.
Способ 1
- Использование энергонезависимого поля
- С помощью
Interlocked.CompareExchange
написать на поле - С помощью
Interlocked.CompareExchange
читать с поля - Это зависит от (возможно, ошибочного) предположения, что после выполнения
Interlocked.CompareExchange(ref v, null, null)
в поле приведет к следующим доступам получить значения, которые по крайней мере так же недавно, как те, которыеInterlocked.CompareExchange
увидел.
Код:
public class SetOnce1<T> where T : class
{
private T _value = null;
public T GetValue() {
if (_value == null) {
// Maybe we got a stale value (from the cache or compiler optimization).
// Read an up-to-date value of that variable
Interlocked.CompareExchange<T>(ref _value, null, null);
// _value contains up-to-date data, because of the Interlocked.CompareExchange call above.
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
}
// _value contains up-to-date data here too.
return _value;
}
public T SetValue(T newValue) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (Interlocked.CompareExchange<T>(ref _value, newValue, null) != null) {
throw new System.Exception("Value already present.");
}
return newValue;
}
}
Способ 2
- Используя
volatile
поле - Использование lonterlocked.CompareExchange
to write the value (with [Joe Duffy](http://www.bluebytesoftware.com/blog/PermaLink,guid,c36d1633-50ab-4462-993e-f1902f8938cc.aspx)'s
#pragmato avoid the compiler warning on passing a volatile value by
ref`). - Чтение значения напрямую, так как поле
volatile
Код:
public class SetOnce2<T> where T : class
{
private volatile T _value = null;
public T GetValue() {
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
public T SetValue(T newValue) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
#pragma warning disable 0420
T oldValue = Interlocked.CompareExchange<T>(ref _value, newValue, null);
#pragma warning restore 0420
if (oldValue != null) {
throw new System.Exception("Value already present.");
}
return newValue;
}
}
Способ 3
- Использование энергонезависимого поля.
- Использование блокировки при записи.
- Использование блокировки при чтении, если мы читаем ноль (мы получим связное значение вне блокировки, так как ссылки читаются атомарно).
Код:
public class SetOnce3<T> where T : class
{
private T _value = null;
public T GetValue() {
if (_value == null) {
// Maybe we got a stale value (from the cache or compiler optimization).
lock (this) {
// Read an up-to-date value of that variable
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
}
return _value;
}
public T SetValue(T newValue) {
lock (this) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (_value != null) {
throw new System.Exception("Value already present.");
}
_value = newValue;
return newValue;
}
}
}
Способ 4
- Использование изменчивого поля
- Запись значения с использованием блокировки.
- Чтение значения напрямую, так как поле является изменчивым (мы получим связное значение, даже если мы не используем блокировку, поскольку ссылки читаются атомарно).
Код:
public class SetOnce4<T> where T : class
{
private volatile T _value = null;
public T GetValue() {
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
public T SetValue(T newValue) {
lock (this) {
if (newValue == null) {
throw new System.ArgumentNullException();
}
if (_value != null) {
throw new System.Exception("Value already present.");
}
_value = newValue;
return newValue;
}
}
}
Другие методы
Я также мог бы использовать Thread.VolatileRead()
читать значение в сочетании с любым из методов письма.
2 ответа
Ну, не уверен насчет изменчивости, но если вы не возражаете против небольшого злоупотребления и вызова второго метода... (также он не зависит от обнуляемости; свободно используется для типов значений). Также избегает проверок нулей в получателе. При записи выполняется только блокировка, поэтому, AFAIK, единственное негативное влияние происходит из-за вызова делегата при получении значения.
public class SetOnce<T>
{
private static readonly Func<T> NoValueSetError = () => { throw new Exception("Value not yet present.");};
private Func<T> ValueGetter = NoValueSetError;
private readonly object SetterLock = new object();
public T SetValue(T newValue)
{
lock (SetterLock)
{
if (ValueGetter != NoValueSetError)
throw new Exception("Value already present.");
else
ValueGetter = () => newValue;
}
return newValue;
}
public T GetValue()
{
return ValueGetter();
}
}
На самом деле, я чувствую себя по-настоящему глупо и чувствую себя немного оскорбительным. Мне было бы интересно увидеть комментарии о потенциальных проблемах, связанных с этим.:)
РЕДАКТИРОВАТЬ: Просто понял, что это означает, что первый звонок SetValue(null)
означает, что "ноль" будет считаться допустимым значением и будет возвращать ноль без исключения. Не уверен, что это то, что вы хотите (я не понимаю, почему null
не может быть допустимым значением, но если вы хотите избежать этого, просто выполните проверку в установщике; нет необходимости в геттере)
EDITx2: если вы все еще хотите ограничить его class
и избегать null
значения, простое изменение может быть:
public class SetOnce<T> where T : class
{
private static readonly Func<T> NoValueSetError = () => { throw new Exception("Value not yet present.");};
private Func<T> ValueGetter = NoValueSetError;
private readonly object SetterLock = new object();
public T SetValue(T newValue)
{
if (newValue == null)
throw new ArgumentNullException("newValue");
lock (SetterLock)
{
if (ValueGetter != NoValueSetError)
throw new Exception("Value already present.");
else
ValueGetter = () => newValue;
}
return newValue;
}
public T GetValue()
{
return ValueGetter();
}
}
Ни один из ваших методов, кроме одного с lock
(#3) будет работать правильно.
Посмотрите:
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
этот код не является атомарным и не потокобезопасным, если не внутри lock
, Еще возможно, что другие наборы потоков _value
в null
между if
а также return
, Что вы можете сделать, это установить локальную переменную:
var localValue = _value;
if (localValue == null) {
throw new System.Exception("Value not yet present.");
}
return localValue;
Но все же это может вернуть стоимость киоска. Вы бы лучше использовать lock
- ясно, просто и быстро.
Редактировать: избегать использования lock(this)
, так как this
виден снаружи, и сторонний код может решить lock
на вашем объекте.
Редактировать 2: если нулевое значение никогда не может быть установлено, просто сделайте:
public T GetValue() {
if (_value == null) {
throw new System.Exception("Value not yet present.");
}
return _value;
}
public T SetValue(T newValue) {
lock (writeLock)
{
if (newValue == null) {
throw new System.ArgumentNullException();
}
_value = newValue;
return newValue;
}
}