Что такое потокобезопасность (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
 }

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

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

EDIT: я имел в виду, если все потоки только читают и используют (не записывают и не изменяют) объект. (за исключением последнего вопроса, где очевидно, что я имел в виду, если потоки читают и пишут). Потому что я не знаю, является ли простой доступ или перечисление потокобезопасным.


person Ben    schedule 26.04.2011    source источник


Ответы (5)


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

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

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

person Lou Franco    schedule 26.04.2011

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

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


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


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

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

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


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

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


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

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

Является ли перечисление всегда/когда-либо потокобезопасным?

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

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

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


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


person Eric Lippert    schedule 26.04.2011
comment
Прежде чем я имею в виду, что что-то было инициализировано до запуска потоков. Под потокобезопасностью я имел в виду, могут ли многие потоки перечислять и/или перебирать одни и те же объекты (массивы, списки,... ‹ - они инициализируются и заполняются раньше) и просто читать из них без проблем? - person Ben; 26.04.2011
comment
@Ben: если там есть начало потока, то есть критическая точка выполнения, и поэтому наблюдаемые побочные эффекты гарантированно будут упорядочены относительно этой критической точки. - person Eric Lippert; 26.04.2011
comment
Нет никакой гарантии, что чтение/запись long/ulong в 64-битной архитектуре будет атомарной? - person James Dunne; 26.04.2011
comment
@James: Спецификация языка ничего не говорит о 64-битном оборудовании. Все, что он говорит, это то, что 32-битные переменные и переменные ссылочного размера должны быть гарантированно атомарными, чтобы реализация соответствовала требованиям. Если вы лично запускаете программу C# на оборудовании, которое дает более надежную гарантию, то эта гарантия предоставляется вам вашим поставщиком оборудования, а не дружественным соседом-поставщиком компилятора C#. Я не собираюсь предъявлять какие-либо претензии от имени неизвестных производителей оборудования. - person Eric Lippert; 26.04.2011
comment
Точно так же модель памяти x86 намного сильнее, чем модель памяти, описанная выше. Поставщики оборудования x86 дают обещания, которые, как они заявляют, выполняют; язык C#, безусловно, не дает никаких обещаний поставщикам оборудования x86. Если вы решите полагаться на обещания, данные вам поставщиками оборудования, это ваше дело. - person Eric Lippert; 27.04.2011
comment
Кроме того, спецификация CLI обещает, что все структуры данных будут атомарными, если они имеют собственный размер слова или меньше, а их адреса выровнены по границам слов. (И это обещает, что вы не получите смещения случайно; вы должны попросить об этом.) Однако язык C# может быть реализован для работы на платформах, отличных от реализаций CLI. - person Eric Lippert; 29.04.2011

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

person sindre j    schedule 26.04.2011

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

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

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.
person Thebigcheeze    schedule 26.04.2011

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

ПС

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

person flq    schedule 26.04.2011