Что такое потокобезопасность (C #)? (Строки, массивы, … ?)

#c# #arrays #string #thread-safety #enumeration

#c# #массивы #строка #потокобезопасность #перечисление

Вопрос:

Я совсем новичок в C #, поэтому, пожалуйста, потерпите меня. Меня немного смущает потокобезопасность. Когда что-то потокобезопасно, а когда что-то нет?

Всегда ли чтение (просто чтение из чего-то, что было инициализировано ранее) из поля потокобезопасно?

 //EXAMPLE
RSACryptoServiceProvider rsa = new RSACrytoServiceProvider();
rsa.FromXmlString(xmlString);  
//Is this thread safe if xml String is predifined 
//and this code can be called from multiple threads?
  

Всегда ли доступ к объекту из массива или списка потокобезопасен (в случае, если вы используете цикл for для перечисления)?

 //EXAMPLE (a is local to thread, array and list are global)
int a = 0;
for(int i=0; i<10; i  )
{
  a  = array[i];
  a -= list.ElementAt(i);
}
  

Всегда ли перечисление потокобезопасно?

 //EXAMPLE
foreach(Object o in list)
{
   //do something with o
 }
  

Могут ли запись и чтение в определенное поле когда-либо приводить к поврежденному чтению (половина поля изменена, а половина по-прежнему неизменна)?

Спасибо за все ваши ответы и время.

РЕДАКТИРОВАТЬ: я имел в виду, если все потоки только читают и используют (не записывают или изменяют) объект. (за исключением последнего вопроса, где очевидно, что я имел в виду, если потоки одновременно читают и записывают). Потому что я не знаю, является ли потокобезопасным простой доступ или перечисление.

Ответ №1:

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

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

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

Ответ №2:

Всегда ли потокобезопасно чтение (просто чтение из чего-то, что было инициализировано ранее) из поля?

Язык C # гарантирует, что операции чтения и записи последовательно упорядочены, когда операции чтения и записи выполняются в одном потоке, как описано в разделе 3.10:


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


События в многопоточной, многопроцессорной системе не обязательно имеют четко определенный согласованный порядок во времени относительно друг друга.Язык C # не гарантирует согласованного упорядочения. Последовательность операций записи, наблюдаемая одним потоком, может оказаться в совершенно другом порядке при наблюдении из другого потока, при условии, что не задействована критическая точка выполнения.

Следовательно, вопрос не подлежит ответу, поскольку он содержит неопределенное слово. Можете ли вы дать точное определение того, что «до» означает для вас в отношении событий в многопоточной, многопроцессорной системе?

Язык гарантирует, что побочные эффекты упорядочены только в отношении критических точек выполнения, и даже тогда не дает никаких надежных гарантий, когда задействованы исключения. Опять же, цитирую из раздела 3.10:


Выполнение программы на C # протекает таким образом, что побочные эффекты каждого выполняющегося потока сохраняются в критических точках выполнения. Побочный эффект определяется как чтение или запись изменяемого поля, запись в энергонезависимую переменную, запись во внешний ресурс и выдача исключения. Критическими точками выполнения, в которых должен быть сохранен порядок этих побочных эффектов, являются ссылки на изменяемые поля, инструкции блокировки, а также создание и завершение потока. […] Порядок побочных эффектов сохраняется в отношении изменчивых операций чтения и записи.

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


Всегда ли доступ к объекту из массива или списка потокобезопасен (в случае, если вы используете цикл for для перечисления)?

Под «потокобезопасностью» вы подразумеваете, что два потока всегда будут получать согласованные результаты при чтении из списка? Как отмечалось выше, язык C # дает очень ограниченные гарантии в отношении наблюдения результатов при чтении из переменных. Можете ли вы дать точное определение того, что для вас означает «потокобезопасность» в отношении энергонезависимого чтения?

Всегда ли перечисление потокобезопасно?

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

Могут ли запись и чтение в определенное поле когда-либо приводить к поврежденному чтению (половина поля изменена, а половина по-прежнему неизменна)?

ДА. Я отсылаю вас к разделу 5.5, в котором говорится:


Операции чтения и записи следующих типов данных являются атомарными: bool, char, byte, sbyte, short, ushort, uint, int, float и ссылочные типы. Кроме того, операции чтения и записи перечисляемых типов с базовым типом в предыдущем списке также являются атомарными. Не гарантируется, что операции чтения и записи других типов, включая long, ulong, double и decimal, а также определяемые пользователем типы, будут атомарными. Помимо библиотечных функций, предназначенных для этой цели, нет гарантии атомарного чтения-изменения-записи, например, в случае увеличения или уменьшения.


Комментарии:

1. Под «до» я подразумеваю, что что-то было инициализировано до запуска потоков. Под «потокобезопасностью» я имел в виду, могут ли многие потоки перечислять и / или выполнять итерации по одним и тем же объектам (массивы, списки, … < — они инициализированы и заполнены «до») и просто читать из них без проблем?

2. @Ben: если там есть запуск потока, значит, существует критическая точка выполнения, и, следовательно, наблюдаемые побочные эффекты гарантированно упорядочены по отношению к этой критической точке.

3. Нет никакой гарантии, что длительное чтение / запись в 64-разрядной архитектуре является атомарным?

4. @James: В спецификации языка нечего сказать о 64-разрядном оборудовании. Все, что здесь говорится, это то, что 32-разрядные переменные и переменные ссылочного размера должны гарантированно быть атомарными, чтобы реализация соответствовала. Если вам лично посчастливилось запускать программу на C # на оборудовании, которое обеспечивает более надежную гарантию, то эта гарантия предоставляется вам вашим поставщиком оборудования, а не поставщиком компилятора C # по соседству. Я не собираюсь предъявлять какие-либо претензии от имени неизвестных производителей оборудования.

5. Аналогично, модель памяти x86 намного, намного сильнее, чем модель памяти, которую я описал выше. Поставщики оборудования x86 дают обещания, которые, как они утверждают, будут выполнены; язык C #, безусловно, не дает никаких обещаний от имени поставщиков оборудования x86. Если вы решите полагаться на обещания, данные вам поставщиками оборудования, это ваше дело.

Ответ №3:

Ну, я обычно предполагаю, что все небезопасно для потоков. Для быстрого и грязного доступа к глобальным объектам в потоковой среде я использую ключевое слово lock(object). .Net имеет обширный набор методов синхронизации, таких как различные семафоры и тому подобное.

Ответ №4:

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

Если вам необходимо это сделать, то вы можете сделать:

 lock(i){
    i.GetElementAt(a)
}
  

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

Что касается перечисления, я буду ссылаться на MSDN:

 The enumerator does not have exclusive access to the collection; therefore, enumerating 
through a collection is intrinsically not a thread-safe procedure. To guarantee thread 
safety during enumeration, you can lock the collection during the entire enumeration. To 
allow the collection to be accessed by multiple threads for reading and writing, you must     
implement your own synchronization.
  

Ответ №5:

Пример отсутствия потокобезопасности: когда несколько потоков увеличивают целое число. Вы можете настроить это таким образом, чтобы у вас было заранее определенное количество приращений. Однако вы можете заметить, что значение int не было увеличено так сильно, как вы ожидали. Что происходит, так это то, что два потока могут увеличивать одно и то же значение целого числа.Это всего лишь пример множества эффектов, которые вы можете наблюдать при работе с несколькими потоками.

PS

Потокобезопасное увеличение доступно через Interlocked.Increment(ref i)