Я разрабатываю класс, который я хочу сделать только для чтения после того, как основной поток будет выполнен с его настройкой, т.е. "заморозить" его. Эрик Липперт называет эту popsicle неизменностью. После его замораживания к нему может быть доступно несколько потоков одновременно для чтения.
Мой вопрос заключается в том, как написать это в потоковом безопасном способе, который реалистично эффективен, т.е. не пытаясь быть излишне умным.
Попытка 1:
public class Foobar
{
private Boolean _isFrozen;
public void Freeze() { _isFrozen = true; }
// Only intended to be called by main thread, so checks if class is frozen. If it is the operation is invalid.
public void WriteValue(Object val)
{
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
public Object ReadSomething()
{
return it;
}
}
Эрик Липперт предположил, что это будет ОК в этой публикации. Я знаю, что записи имеют семантику выпуска, но, насколько я понимаю, это относится только к упорядочению, и это не обязательно означает, что все потоки будут видеть значение сразу после записи. Может ли кто-нибудь подтвердить это? Это означало бы, что это решение не является потокобезопасным (это, конечно, не единственная причина).
Попытка 2:
Выше, но используя Interlocked.Exchange
, чтобы убедиться, что значение действительно опубликовано:
public class Foobar
{
private Int32 _isFrozen;
public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }
public void WriteValue(Object val)
{
if (_isFrozen == 1)
throw new InvalidOperationException();
// write ...
}
}
Преимущество здесь состояло в том, чтобы гарантировать, что ценность публикуется без ущерба для каждого чтения. Если ни один из чтения не перемещается перед записью в _isFrozen, поскольку метод Interlocked использует полный барьер памяти, я бы предположил, что это потокобезопасно. Тем не менее, кто знает, что сделает компилятор (и в соответствии с разделом 3.10 спецификации С#, который кажется довольно много), поэтому я не знаю, является ли это потокобезопасным.
Попытка 3:
Также читайте, используя Interlocked
.
public class Foobar
{
private Int32 _isFrozen;
public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }
public void WriteValue(Object val)
{
if (Interlocked.CompareExchange(ref _isFrozen, 0, 0) == 1)
throw new InvalidOperationException();
// write ...
}
}
Определенно потокобезопасный, но кажется немного расточительным, что нужно делать обмен обмена для каждого чтения. Я знаю, что это накладные расходы, вероятно, минимальные, но я ищу достаточно эффективный метод (хотя, возможно, это и есть).
Попытка 4:
Используя volatile
:
public class Foobar
{
private volatile Boolean _isFrozen;
public void Freeze() { _isFrozen = true; }
public void WriteValue(Object val)
{
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
}
Но Джо Даффи заявил "sayonara volatile, поэтому я не буду рассматривать это решение.
Попытка 5:
Заблокировать все, кажется немного переполненным:
public class Foobar
{
private readonly Object _syncRoot = new Object();
private Boolean _isFrozen;
public void Freeze() { lock(_syncRoot) _isFrozen = true; }
public void WriteValue(Object val)
{
lock(_syncRoot) // as above we could include an attempt that reads *without* this lock
if (_isFrozen)
throw new InvalidOperationException();
// write ...
}
}
Также кажется, что он определенно потокобезопасен, но имеет больше накладных расходов, чем использование вышеперечисленного подхода, поэтому я бы предпочел попытку 3 над этим.
И тогда я могу придумать хотя бы еще (я уверен, что их еще много):
Попытка 6: использовать Thread.VolatileWrite
и Thread.VolatileRead
, но они предположительно немного на тяжелой стороне.
Попытка 7: использовать Thread.MemoryBarrier
, кажется немного слишком внутренней.
Попытка 8: создать неизменяемую копию - не хотите этого делать
Резюмируя:
- какую попытку вы использовали бы и почему (или как бы вы это сделали, если бы совсем другое)? (то есть, что является лучшим способом для публикации значения один раз, который затем читается одновременно, будучи достаточно эффективным без чрезмерного "умного"?)
- Использует ли в моделях памяти .NET версию "release", что все остальные потоки видят обновления (согласованность кэшей и т.д.)? Я вообще не хочу слишком много думать об этом, но приятно иметь понимание.
EDIT:
Возможно, мой вопрос был не ясен, но я смотрю, в частности, по причинам, почему эти попытки являются хорошими или плохими. Обратите внимание, что я говорю здесь о сценарии одного автора, который пишет, затем замораживает перед любыми параллельными чтениями. Я считаю, что попытка 1 в порядке, но я хотел бы точно знать, почему (поскольку мне кажется, что чтение может быть оптимизировано как-то, например). Мне все равно, неважно, хороша ли эта практика, но больше о фактическом аспекте ее резьбы.
Большое спасибо за ответ на полученный вопрос, но я решил отметить это как ответ сам, потому что я чувствую, что полученные ответы не совсем отвечают на мой вопрос, и я не хочу давать впечатление кому-либо, посещающему сайт что отмеченный ответ верен просто потому, что он автоматически помечен как таковой из-за истечения срока годности. Кроме того, я не думаю, что ответ с наибольшим количеством голосов был подавляющим большинством голосов, недостаточно, чтобы отметить его автоматически в качестве ответа.
Я все еще склоняюсь к правильной попытке № 1, однако мне бы понравились некоторые авторитетные ответы. Я понимаю, что у x86 есть сильная модель, но я не хочу (и не должен) кодировать для конкретной архитектуры, после всего, что одна из приятных вещей о .NET.
Если вы сомневаетесь в ответе, перейдите на один из подходов к блокировке, возможно, с приведенными здесь оптимизациями, чтобы избежать много споров о блокировке.