Как заморозить кусочек в .NET(сделать класс неизменным)

Я разрабатываю класс, который я хочу сделать только для чтения после того, как основной поток будет выполнен с его настройкой, т.е. "заморозить" его. Эрик Липперт называет эту 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.

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

Ответ 1

Может быть, немного из темы, но просто из любопытства:) Почему бы вам не использовать "настоящую" неизменность? например make Freeze() возвращает неизменяемую копию (без "методов записи" или любую другую возможность изменить внутреннее состояние) и используя эту копию вместо исходного объекта. Вы можете даже пойти без изменения состояния и вернуть новую копию (с измененным состоянием) в каждую операцию записи (afaik - это класс строк). "Настоящая неизменность" по своей сути является надежной в потоковом режиме.

Ответ 2

Я прогоняю за попытку 5, использую реализацию lock (this).

Это самый надежный способ сделать эту работу. Могут использоваться блокировки чтения/записи, но очень мало. Просто используйте обычный замок.

При необходимости вы можете улучшить "замороженную" производительность, сначала проверив _isFrozen, а затем закрепив:

void Freeze() { lock (this) _isFrozen = true; }
object ReadValue()
{
    if (_isFrozen)
        return Read();
    else
        lock (this) return Read();
}
void WriteValue(object value)
{
    lock (this)
    {
        if (_isFrozen) throw new InvalidOperationException();
        Write(value);
    }
}

Ответ 3

Если вы действительно создаете, заполняете и замораживаете объект, прежде чем показывать его другим потокам, то вам не нужно ничего особенного для решения проблемы безопасности потоков (сильная модель памяти .NET уже является вашей гарантией), поэтому решение 1 действительно.

Но если вы дадите размороженный объект другому потоку (или если вы просто создаете свой класс, не зная, как его будут использовать пользователи), то, используя версию, решение, которое возвращает новый полностью неизменяемый экземпляр, вероятно, лучше. В этом случае экземпляр Mutable похож на StringBuilder, а неизменяемый экземпляр подобен строке. Если вам нужна дополнительная гарантия, изменяемый экземпляр может проверить поток создателя и исключить исключения, если он используется из любого другого потока (во всех методах... во избежание возможных частичных чтений).

Ответ 4

Попытка 2 является потокобезопасной на x86 и других процессорах с сильной моделью памяти, но как бы я это сделал, это сделать проблему безопасности потоков проблемой потребителей, потому что вам не удастся эффективно ее выполнять внутри потребляемого кода. Рассмотрим:

if(!foo.frozen)
{
    foo.apropery = "avalue";
}

безопасность потока свойства frozen и защитный код в установщике apropery не имеет большого значения, потому что даже они отлично защищены потоком, у вас все еще есть состояние гонки. Вместо этого я бы написал это как

lock(foo)
{
    if(!foo.frozen)
    {
        foo.apropery = "avalue";
    }
}

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

Ответ 5

# 1 - читатель не threadsafe - я считаю, что проблема будет в стороне читателя, а не в писателе (код не показан)
# 2 - читатель не threadsafe - так же, как # 1
# 3 - обещание, проверка чтения может быть оптимизирована для большинства случаев (когда тайники CPU синхронизированы)

Попытка 3:

Также читайте, используя блокировку.

public class Foobar {

  private object _syncRoot = new object();
  private int _isFrozen = 0; // perf compiler warning, but training code, so show defaults

  // Why Exchange to 1 then throw away result.  Best to just increment.
  //public void Freeze() { Interlocked.Exchange(ref _isFrozen, 1); }
  public void Freeze() { Interlocked.Increment(ref _isFrozen); }

  public void WriteValue(Object val) {
       // if this core can see _isFrozen then no special lock or sync needed
       if (_isFrozen != 0)
           throw new InvalidOperationException();

       lock(_syncRoot) {
           if (_isFrozen != 0)
               throw new InvalidOperationException(); // the 'throw' is 100x-1000x more costly than the lock, just eat it
           _val = val;
       }

  }

  public object Read() {
       // frozen is one-way, if one-way state has been published 
       // to my local CPU cache then just read _val.  
       // There are very strange corner cases when _isFrozen and _val fields are in 
       // different cache lines, but should be nearly impossible to hit unless
       // dealing with very large structs (make it more likely to cross 
       // 4k cache line).
       if (_isFrozen != 0) 
           return _val;

       // else
       lock(_syncRoot) { // _isFrozen is 0 here
           if (_isFrozen != 0) // if _isFrozen is 1 here we just collided with writer using lock on other thread, or our CPU cache was out of sync and lock() forced the dirty cache line to be read from main memory
              return _val;

           throw new InvalidOperationException(); // throw is 100x-1000x more expensive than lock, eat the cost of lock
       }          
  }

}

Сообщение Джо Даффи о "volatile is dead", я думаю, относится к его архитектуре CLR/OS следующего поколения и для CLR на ARM. Те из нас, кто делает многоядерный x64/x86, я считаю, что волатильность в порядке. Если первоочередной задачей является первичность, я предлагаю вам измерить код выше и сравнить его с изменчивым.

В отличие от других людей, отправляющих ответы, я бы не прыгнул прямо в lock(), если у вас много читателей (3 или более потока, которые могут одновременно читать один и тот же объект). Но в вашем примере вы смешиваете персид-чувствительный вопрос с исключениями, когда происходит столкновение, что не имеет особого смысла. Если вы используете исключения, вы также можете использовать другие конструкции более высокого уровня.

Если вам нужна полная безопасность, но вам нужно оптимизировать для большого количества одновременных считывателей, замените lock()/Monitor на ReaderWriterLockSlim.

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

Если записано несколько раз, но сохраняется только одно значение - в Rx, который является "новым ReplaySubject (bufferSize: 1)". Если вы попробуете, вы можете быть удивлены, как быстро это. В то же время я приветствую вашу попытку изучить этот уровень детализации.

Если вы хотите пойти беззаконно, перейдите через ваше отвращение к Thread.MemoryBarrier(). Это очень важно. Но он имеет те же самые переходы, что и volatile, как описано Джо Даффи - он был разработан как подсказка для компилятора и процессора, чтобы предотвратить переупорядочение чтения в памяти (что занимает много времени в терминах процессора, поэтому они агрессивно переупорядочиваются, когда нет намеки присутствуют). Когда это переупорядочение сочетается с конструкциями CLR, такими как автоматическое включение функций, и вы можете наблюдать очень удивительное поведение на уровне памяти и регистрации. MemoryBarrier() просто отключает однопотоковые допуски доступа к памяти, которые используют процессор и CLR большую часть времени.

Ответ 6

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

Хорошо, теперь я лучше понимаю, что вы делаете и ищете в ответ. Позвольте мне подробно остановиться на моем предыдущем ответе на использование блокировок, сначала обратившись к каждой из ваших попыток.

Попытка 1:

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

Помимо потока, имеющего кэшированную копию внутреннего состояния этого списка, есть еще одна проблема concurrency, с которой вы должны иметь дело. Вы должны подумать о переупорядочении записи по авторскому потоку. Вы, например, решение, не имеете достаточного кода для меня, чтобы решить эту проблему, но процесс передачи этого "замороженного" списка в другой поток является сутью проблемы. Используете ли вы Interlocked.Exchange или записываете в неустойчивое состояние?

Я по-прежнему утверждаю, что это не лучший подход просто потому, что нет гарантии, что другой поток не видел экземпляр, пока он мутирует.

Попытка 2:

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

Попытка 3:

Это гарантирует исключение, если поток попытается изменить замороженный список. Однако он не утверждает, что чтение допустимо только для замороженного экземпляра. Это, ИМХО, так же плохо, как доступ к нашей переменной _isFrozen с атомарными и неатомными аксессуарами. Если вы скажете, что важно защищать записи, тогда вы всегда должны защищать чтение. Один без другого просто "странный".

С учетом моего собственного чувства к написанию кода, который пишет gaurds, но не читает, это приемлемый подход, учитывая ваши конкретные применения. У меня есть один писатель, я пишу, я замерзаю, а затем я делаю это доступным для читателей. В этом сценарии код работает правильно. Вы полагаетесь на атомную операцию на наборе _isFrozen, чтобы обеспечить требуемый барьер памяти до передачи класса в другой поток.

В двух словах этот подход работает, но опять же, если поток имеет экземпляр, который не заморожен, он сломается.

Попытка 4:

Хотя в глубине души это почти то же самое, что и попытка 3 (учитывая одного автора), есть одна большая разница. В этом примере, если вы проверите _isFrozen в читателе, то для каждого доступа потребуется барьер памяти. Это лишние накладные расходы после того, как список заморожен.

Тем не менее это имеет ту же проблему, что и в попытке 3, поскольку в ходе чтения нет утверждений о состоянии _isFrozen, поэтому производительность должна быть одинаковой в вашем примере использования.

Попытка 5:

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

Попытка 6:

По существу, это то же самое, что и # 4.

Попытка 7:

Вы можете решить свои конкретные потребности с помощью Thread.MemoryBarrier. По существу, используя код из "Попытка 1", вы создаете экземпляр, вызываете Freeze(), добавляете свой Thread.MemoryBarrier, а затем совместно используете экземпляр (или делите его внутри блокировки). Это должно работать отлично, снова только в ограниченном использовании.

Попытка 8:

Не зная об этом больше, я не могу сообщить о стоимости копии.

Резюме

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

По словам знаменитого мастера-джедая:

Либо делать, либо нет, нет попытки.

То же самое касается безопасности потоков. Класс должен быть потокобезопасным или нет. Используя этот подход, вы остаетесь либо с использованием моего увеличения попытки 5, либо с использованием попытки 7. Учитывая выбор, я бы никогда не рекомендовал № 7.

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

Мои предпочтения - все классы, разделяемые более чем одним потоком, являются одним из двух типов:

  • Полностью непреложный.
  • Полностью поточно-безопасный.

Поскольку список popsicle не является неизменным по дизайну, он не соответствует # 1. Поэтому, если вы собираетесь делиться объектом по потокам, он должен соответствовать # 2.

Надеюсь, что все это разглашение объясняет мои рассуждения:)

_syncRoot

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

Ответ 7

Является ли построенная и написанная вещь, затем постоянно замороженная и прочитанная несколько раз?

Или вы замерзаете и размораживаете его и несколько раз замораживаете?

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

Или, если это последний, то прецедентом, о котором вы должны остерегаться, является:

  • Основной поток вызывает метод записи, находит, что он не замерз, и поэтому начинает писать
  • Прежде чем запись закончится, кто-то пытается заморозить объект, а затем читает его, а другой (основной) поток все еще записывает

В последнем случае Google показывает много результатов для нескольких писателей с несколькими читателями, которые могут показаться вам интересными.

Ответ 8

В общем, каждый измененный объект должен иметь точно один четко определенный "владелец"; общие объекты должны быть неизменными. Popsicles не должны быть доступны несколькими потоками до тех пор, пока они не будут заморожены.

Лично мне не нравятся формы иммунитета эфесов с открытым методом "замораживания". Я думаю, что более чистый подход состоит в том, чтобы иметь методы AsMutable и AsImmutable (каждый из которых просто возвращал бы объект без модификации, когда это было необходимо). Такой подход может обеспечить более надежный promises о неизменности. Например, если "нерасширенный измененный объект" мутируется, пока его член AsImmutable вызывается (поведение, которое противоречит объекту, "не разделенному" ), состояние данных в копии может быть неопределенным, но все, что было возвращено, было бы неизменным. Напротив, если один поток заморозил объект, а затем предположил, что он неизменен, а другой поток пишет ему, "неизменный" объект может в конечном итоге измениться после его замораживания и считывания его значений.

Изменить

Основываясь на дальнейшем описании, я предлагаю иметь код, который пишет объект, сделать это внутри блокировки монитора, а процедура замораживания выглядит примерно так:

public Thingie Freeze(void) // Returns the object in question
{
  if (isFrozen) // Private field
    return this;
  else
    return DoFreeze();
}

Thingie DoFreeze(void)
{
  if (Monitor.TryEnter(whatever))
  {
    isFrozen = true;
    return this;
  }
  else if (isFrozen)
    return this;
  else
    throw new InvalidOperationException("Object in use by writer");
}

Метод Freeze может быть вызван любым числом раз любым количеством потоков; он должен быть достаточно коротким, чтобы быть встроенным (хотя я его не профилировал), и поэтому у него почти нет времени на выполнение. Если первый доступ к объекту в любом потоке осуществляется с помощью метода Freeze, который должен гарантировать правильную видимость под любой разумной моделью памяти (даже если поток не видел обновлений для объекта, выполняемого потоком, который был создан и изначально заморозив его, он выполнил бы TryEnter, что гарантировало бы барьер памяти, и после этого не удалось заметить, что объект был заморожен и возвратил его.

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

Объект, используемый для блокировки, должен быть чем-то, что удерживается объектом, подлежащим замораживанию. Если объект, который должен быть заморожен, не содержит чисто частной ссылки на что-либо, можно либо заблокировать this, либо создать частный объект исключительно для целей блокировки. Обратите внимание, что безопасно оставлять "введенные" блокировки монитора без очистки; GC просто забудет о них, поскольку, если ссылок на блокировку не существует, никто не захочет (или даже может спросить), был ли замок введен в то время, когда он был оставлен.

Ответ 9

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

Попытка 9:

public class Foobar
{
    private readonly Object _syncRoot = new Object();
    private object _val;
    private Boolean _isFrozen;

    private Action<object> WriteValInternal;

    public void Freeze() { _isFrozen = true; }

    public Foobar()
    {
        WriteValInternal = BeforeFreeze;
    }

    private void BeforeFreeze(object val)
    {
        lock (_syncRoot)
        {
            if (_isFrozen == false)
            {
                //Write the values....
                _val = val;
                //...
                //...
                //...
                //and then modify the write value function
                WriteValInternal = AfterFreeze;
                Freeze();
            }
            else
            {
                throw new InvalidOperationException();
            }
        }
    }

    private void AfterFreeze(object val)
    {
        throw new InvalidOperationException();
    }

    public void WriteValue(Object val)
    {
        WriteValInternal(val);
    }

    public Object ReadSomething()
    {
        return _val;
    }
}

Ответ 11

вы можете достичь этого, используя POST Sharp

возьмите один интерфейс

public interface IPseudoImmutable
{

    bool IsFrozen { get; }


    bool Freeze();
}

то вы получите свой атрибут из InstanceLevelAspect, подобный этому

 /// <summary>
/// implement by divyang
/// </summary>
[Serializable]
[IntroduceInterface(typeof(IPseudoImmutable),
    AncestorOverrideAction = InterfaceOverrideAction.Ignore, OverrideAction = InterfaceOverrideAction.Fail)]
public class PseudoImmutableAttribute : InstanceLevelAspect, IPseudoImmutable
{

    private volatile bool isFrozen;

    #region "IPseudoImmutable"


    [IntroduceMember]
    public bool IsFrozen
    {
        get
        {
            return this.isFrozen;
        }
    }


    [IntroduceMember(IsVirtual = true, OverrideAction = MemberOverrideAction.Fail)]
    public bool Freeze()
    {
        if (!this.isFrozen)
        {
            this.isFrozen = true;
        }

        return this.IsFrozen;
    }

    #endregion


    [OnLocationSetValueAdvice]
    [MulticastPointcut(Targets = MulticastTargets.Property | MulticastTargets.Field)]
    public void OnValueChange(LocationInterceptionArgs args)
    {
        if (!this.IsFrozen)
        {
            args.ProceedSetValue();
        }
    }
}


public class ImmutableException : Exception
{
    /// <summary>
    /// The location name.
    /// </summary>
    private readonly string locationName;

    /// <summary>
    /// Initializes a new instance of the <see cref="ImmutableException"/> class.
    /// </summary>
    /// <param name="message">
    /// The message.
    /// </param>
    public ImmutableException(string message)
        : base(message)
    {
    }


    public ImmutableException(string message, string locationName)
        : base(message)
    {
        this.locationName = locationName;
    }


    public string LocationName
    {
        get
        {
            return this.locationName;
        }
    }
}

то примените в своем классе, как это

    [PseudoImmutableAttribute]
public class TestClass
{



    public string MyString { get; set; }




    public int MyInitval { get; set; }
}

затем запустите его в нескольких потоках

 /// <summary>
/// The program.
/// </summary>
public class Program
{
    /// <summary>
    /// The main.
    /// </summary>
    /// <param name="args">
    /// The args.
    /// </param>
    public static void Main(string[] args)
    {
        Console.Title = "Divyang Demo ";
        var w = new Worker();
        w.Run();
        Console.ReadLine();
    }
}


internal class Worker
{

    private object SyncObject = new object();


    public Worker()
    {
        var r = new Random();
        this.ObjectOfMyTestClass = new MyTestClass { MyInitval = r.Next(500) };
    }


    public MyTestClass ObjectOfMyTestClass { get; set; }


    public void Run()
    {

        Task readWork;

        readWork = Task.Factory.StartNew(

            action: () =>
                {
                    for (;;)
                    {
                        Task.Delay(1000);
                        try
                        {
                            this.DoReadWork();
                        }
                        catch (Exception exception)
                        {
                            // Console.SetCursorPosition(80,80);
                            // Console.SetBufferSize(100,100);
                            Console.WriteLine("Read Exception : {0}", exception.Message);
                        }
                    }
                    // ReSharper disable FunctionNeverReturns
                });


        Task writeWork;


        writeWork = Task.Factory.StartNew(

            action: () =>
                {
                    for (int i = 0; i < int.MaxValue; i++)
                    {
                        Task.Delay(1000);
                        try
                        {
                            this.DoWriteWork();
                        }
                        catch (Exception exception)
                        {
                            Console.SetCursorPosition(80, 80);
                            Console.SetBufferSize(100, 100);
                            Console.WriteLine("write Exception : {0}", exception.Message);
                        }

                        if (i == 5000)
                        {

                            ((IPseudoImmutable)this.ObjectOfMyTestClass).Freeze();

                        }
                    }
                });

        Task.WaitAll();
    }

    /// <summary>
    /// The do read work.
    /// </summary>
    public void DoReadWork()
    {
        // ThreadId  where reading is done
        var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;

        // printing on screen
        lock (this.SyncObject)
        {
            Console.SetCursorPosition(0, 0);
            Console.SetBufferSize(290, 290);
            Console.WriteLine("\n");
            Console.WriteLine("Read Start");
            Console.WriteLine("Read => Thread Id: {0} ", threadId);
            Console.WriteLine("Read => this.objectOfMyTestClass.MyInitval: {0} ", this.ObjectOfMyTestClass.MyInitval);
            Console.WriteLine("Read => this.objectOfMyTestClass.MyString: {0} ", this.ObjectOfMyTestClass.MyString);


            Console.WriteLine("Read End");
            Console.WriteLine("\n");
        }
    }

    /// <summary>
    /// The do write work.
    /// </summary>
    public void DoWriteWork()
    {
        // ThreadId  where reading is done
        var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;

        // random number generator
        var r = new Random();
        var count = r.Next(15);

        // new value for Int property
        var tempInt = r.Next(5000);
        this.ObjectOfMyTestClass.MyInitval = tempInt;

        // new value for string Property
        var tempString = "Randome" + r.Next(500).ToString(CultureInfo.InvariantCulture);

        this.ObjectOfMyTestClass.MyString = tempString;


        // printing on screen
        lock (this.SyncObject)
        {
            Console.SetBufferSize(290, 290);
            Console.SetCursorPosition(125, 25);

            Console.WriteLine("\n");
            Console.WriteLine("Write Start");
            Console.WriteLine("Write => Thread Id: {0} ", threadId);
            Console.WriteLine("Write => this.objectOfMyTestClass.MyInitval: {0} and New Value :{1} ", this.ObjectOfMyTestClass.MyInitval, tempInt);
            Console.WriteLine("Write => this.objectOfMyTestClass.MyString: {0} and New Value :{1} ", this.ObjectOfMyTestClass.MyString, tempString);
                          Console.WriteLine("Write End");
            Console.WriteLine("\n");
        }
    }
}





but still it will allow you to change property like array ,list . but if you apply more login in that then it may work for all type of property and field