Потокобезопасность списка <T>с одним писателем, без счетчиков - PullRequest
5 голосов
/ 01 августа 2011

Проходя по некоторому коду базы данных в поисках ошибки, не связанной с этим вопросом, я заметил, что в некоторых местах List<T> использовался не по назначению.В частности:

  1. Было много потоков, одновременно обращающихся к List в качестве считывателей, но использующих индексы в list вместо enumerators.
  2. В list.
  3. был один писатель. синхронизация была нулевая , читатели и писатели одновременно обращались к list, но из-за структуры кода последний элемент никогда не будет доступен, пока не будет возвращен метод, который выполнил Add().
  4. Ни один элемент не был удален из list.

с помощью C # документация, это не должно быть потокобезопасным.И все же это никогда не подводило.Мне интересно, из-за конкретной реализации List (внутренне я предполагаю, что это массив , который перераспределяет , когда ему не хватает места), это 1-Writer 0-enumerator n-reader Сценарий только для добавления случайно поточно-безопасный или есть какой-то маловероятный сценарий, когда это может взорваться в текущей реализации .NET4 ?

редактировать: важные детали я не учел, читая некоторые ответы.Читатели обрабатывают List и его содержимое только для чтения.

Ответы [ 5 ]

2 голосов
/ 01 августа 2011

Это может и будет дуть. Просто еще нет. Устаревшие индексы, как правило, первое, что идет. Это будет дуть только тогда, когда вы этого не хотите. Возможно, вам сейчас повезло.

Поскольку вы используете .Net 4.0, я бы предложил изменить список на подходящий набор из System.Collections.Concurrent, который гарантированно безопасен для потоков. Я также избегал бы использования индексов массива и переключался бы на ConcurrentDictionary, если вам нужно что-то искать:

http://msdn.microsoft.com/en-us/library/dd287108.aspx

1 голос
/ 01 августа 2011

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

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

for (int index =0 ; index < list.Count; index++)
{
    MyClass myClass = list[index];//ok we are just reading the value from list
    myClass.SomeInteger++;//boom the same variable will be updated from another threads...
}

В этом примере речь идет не о поточно-безопасных самих списках, а об общих переменных, представленных в списке.

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

0 голосов
/ 17 января 2012

Прежде всего, к некоторым постам и комментариям, с каких пор документация была надежной?

Во-вторых, этот ответ больше относится к общему вопросу, чем к особенностям ОП.

Я согласен с MrFox в теории, потому что все сводится к двум вопросам:

  1. Реализован ли класс List в виде плоского массива?

Если да, то:

  1. Может ли инструкция записи быть выгружена в середине записи>

Я полагаю, что это не так - полная запись произойдет до того, как кто-либо сможет прочитать этот DWORD или что-то еще. Другими словами, никогда не случится, что я запишу два из четырех байтов DWORD, а затем вы прочитаете 1/2 нового значения и 1/2 старого.

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

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

Мой собственный опыт показывает, что чтение одного элемента из списка безопасно с помощью индекса без блокировки потоков. Хотя это все ИМХО, так что принимайте это за то, что оно стоит.

В худшем случае, например, если вам нужно перебрать список, лучше всего сделать следующее:

  1. заблокировать список
  2. создать массив одинакового размера
  3. используйте CopyTo () для копирования списка в массив
  4. разблокировать список
  5. итерируйте массив вместо списка.

in (как вы называете .net) C ++:

  List<Object^>^ objects = gcnew List<Object^>^();
  // in some reader thread:
  Monitor::Enter(objects);
  array<Object^>^ objs = gcnew array<Object^>(objects->Count);
  objects->CopyTo(objs);
  Monitor::Exit(objects);
  // use objs array

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

Только наперед: если вам нужна быстрая система, блокировка потоков - ваш злейший враг. Используйте взамен ZeroMQ . Я могу говорить по опыту, синхронизация на основе сообщений - верный путь.

0 голосов
/ 01 августа 2011

Безопасность потоков имеет значение только тогда, когда данные изменяются более одного раза за раз. Количество читателей не имеет значения. Даже когда кто-то пишет, а кто-то читает, читатель либо получает старые данные, либо новые, он все равно работает. Тот факт, что доступ к элементам возможен только после возврата Add (), предотвращает чтение частей элемента по отдельности. Если вы начнете использовать метод Insert (), читатели могут получить неверные данные.

0 голосов
/ 01 августа 2011

Из этого следует, что если архитектура имеет 32 бита, запись поля размером более 32 бит, такого как long и double, не является поточно-безопасной операцией; см. документацию для System.Double :

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

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

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

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

...