Ответ 1
Чтобы убедиться в том, что вам не нужна ни тщательность, ни оптимизация, используйте Interlocked.Exchange()
для обновления поля. Тогда у вас будет соответствующий барьер памяти при записи, без расхода volatile
на каждый прочитанный.
У меня есть приложение, в котором я хочу, чтобы несколько потоков читали список. Я хочу периодически обновлять список новыми данными. Когда список обновляется, я полагаю, что могу создать новый список и заменить его на старый. Пример:
private List<string> _list = new List<string>();
private void UpdateList()
{
var newList = new List<string>(QueryList(...));
_list = newList;
}
private void ThreadRun()
{
foreach (var item in _list)
{
// process item...
}
}
В методе UpdateList создается новый список, а ссылка _list заменяется новым списком. По моему мнению, любой существующий поток по-прежнему будет содержать ссылку на старый список (это нормально для меня), любой новый поток подберет новый список. В конце концов, все потоки завершатся, и старый список будет в конечном итоге собранным мусором. Есть ли какая-либо блокировка, требуемая в этом коде, или есть что-то, что мне нужно, чтобы обеспечить безопасный многопоточный доступ?
Чтобы убедиться в том, что вам не нужна ни тщательность, ни оптимизация, используйте Interlocked.Exchange()
для обновления поля. Тогда у вас будет соответствующий барьер памяти при записи, без расхода volatile
на каждый прочитанный.
Вы делаете экземпляры списка неизменяемыми, и это только изменчивое состояние вызывает проблемы.
Ваш код в порядке, но вы должны рассмотреть маркировку _list
как volatile
. Это будет означать, что все чтения получают последнюю версию, а компилятор/джиттер/процессор не будут оптимизировать прочтение.
В качестве альтернативы вы можете поместить Thread.MemoryBarrier()
непосредственно перед тем, как назначить новый список, чтобы убедиться, что все записи в новый список были зафиксированы до его публикации, но это не проблема архитектуры x86.
Назначение является атомарным, ваш код в порядке. Возможно, вы захотите рассмотреть маркировку _list
как volatile, хотя для обеспечения того, чтобы любые потоки, запрашивающие переменную, получили самую последнюю версию:
private volatile List<string> _list = new List<string>();
Если вы используете .NET 4 или +, вы можете использовать новые новые потокобезопасные коллекции...
BlockingCollection<string> list = new BlockingCollection<string>();
Класс BlockingCollection имеет поточно-безопасные методы для добавления и удаления элементов, скорее, как шаблон дизайна производителя.
Темы могут добавлять, а другие потоки могут удалять члены списка без накладных расходов на программирование.
Кроме того, он позволяет делать такие вещи...
foreach (string i in list)
{
list.Take();
list.Add(i + 200);
}
Этот код удаляет и добавляет в коллекцию, пока ее перечислитель работает, что никогда не может быть сделано в С# до .NET 4. Нет необходимости объявлять его изменчивым.
foreach (string i in list)
{
new Task(() =>list.Take()).Start();
new Task(() =>list.Add(i + 200)).Start();
}
В этом фрагменте запускаются нити N * 2, которые работают в одном списке...
Различное поведение, подразумеваемое при использовании коллекций Concurrnt, может избавить вас от необходимости иметь два списка.
Вам нужно прочитать модель памяти .net, так как в целом порядок прав не определен на процессоре, поэтому новое значение _list может быть записано в общую память, а часть нового созданного объекта указана to _list все еще находится в очереди на запись процессоров.
Переопределение прав - это боль, о которой нужно подумать, поэтому я хотел бы вставить Thread.MemoryBarrier(); как это.
private void UpdateList()
{
var newList = new List<string>(QueryList(...));
Thread.MemoryBarrier();
_list = newList;
}
Подробнее см. Threading на веб-странице С# Джозефом Альбахари.
Однако я думаю, что на большинстве "нормальных" процессоров ваш код будет работать как написанный.