12/7/2008 3:19:59 PM

Вообще-то я давненько уже писал:
- “О пользе IEnumerable
- “Еще о коллекциях и многопоточности.

Сегодня встретил вот постинг Эрика Люпперта (человек имеет прямое отношение к созданию C#) относительно того, когда же стоит пользоваться массивами (которые Array).
По его мнению – никогда. Причины перекликаются с теми, о которых писал и я, но почитать интересно.

Ссылка: http://blogs.msdn.com/ericlippert/archive/2008/09/22/arrays-considered-somewhat-harmful.aspx

2/6/2008 9:50:21 PM

Вот в комментариях к прошлому постингу был интересный вопрос о том, что не оставить ли все "заморочки" с многопоточностью на совести программиста?
Действительно, с одной стороны мысль разумная. С другой же стороны речь идет именно о гарантиях. Ну, как пример возьмем тот же самый объект:

public sealed class TestObject    
{    
    public IEnumerable<string> Users { get; set; }
}

Какого рода действия здесь может предпринять программист для того, чтобы чувствовать себя уверенно в многопоточной среде?
Самое простое и часто распространенное - синхронизация. При этом, понятное дело, нужно синхронизировать как операции чтения, так и операции записи. То есть, если коллекцию кто-то пишет, то ее нельзя читать. А если коллекцию кто-то читает, то ее нельзя писать. Тогда случае самого примитивного lock'а мы сводим "на нет" всю нашу многопоточность: одновременно с коллекцией может работать только один поток, тот, который захватил этот самый монитор (lock).

Можно придумать более хитроумный подход, когда мы позволяем читать коллекцию многопоточно, а блокировать коллекцию будем только в случае ее изменения. Естественно, до начала внесения изменений все, кто читал коллекцию должны завершить свои операции (те, кто еще не начал - ждут), затем мы вносим изменения, а затем снова разрешаем операции чтения.
Подобные механизмы обычно реализуются через две очереди, одна содержит операции чтения и задачи из нее выполняются во многих потоках, вторая - операции записи, задачи из которой выполняются эксклюзивно. Такой подход легко реализуется через, скажем, CCR, а так же специальная инфраструктура для этого имеется в свободно распространяемой библиотеке Wintellect Power Threads, написанной Джефри Рихтером.
Однако и в случае этого подхода нас ожидает ряд сюрпризов. Один из них - продолжительность операции чтения коллекции. Представьте, что енумерируя TestObject.Users я для каждого элемента коллекции делаю запрос к веб-сервису. Допустим, я получаю данные по пользователю, обрабатываю их, вношу изменения в соответствующие статистические данные, делаю биллинг и т.д. Операции-то небыстрые. Я енумерирую, а все остальные операции просто стоят и ждут. Потому, что следующая на очереди - операция изменения коллекции. Она ждет, пока я закончу читать, остальные операции чтения ждут, пока операция записи выполнит свою работу эксклюзивно. Семеро одного ждут.
Да, я могу вместо енумерирования Users каждый раз создавать на его основе новую коллекцию, или лист, и потом тормозить с ней столько времени, сколько хочу, не мешая другим. Но - я вынужден буду делать это каждый раз, когда я хочу работать с коллекцией Users. Чтобы не заставлять ждать других, я каждый раз при чтении создаю новую копию коллекции. Плюс мне, человеку, в общем-то постороннему, нужно знать и помнить, что работать надо так и только так. Плюс - достаточно сложная инфраструктура с задачами, очередями... Точнее, минусы все это, в общем-то ;)

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

2/2/2008 12:01:00 AM

Недавно я писал о преимуществах работы с IEnumerable в сравнении со всякими листами и массивами, сегодня немного продолжу эту же тему.

Мысль навеяна сниппетом "invoke" из Visual Studio, который (совершенно правильно) предлагает возбуждать событие вот таким вот образом:

 

public event EventHandler StateChanged;
private void OnStateChanged()
{
    EventHandler changedHandler = StateChanged;
    if (changedHandler != null)
    {
        changedHandler(this, EventArgs.Empty);
    }
}

 

вместо "привычного" и часто встречающегося:

 

public event EventHandler StateChanged;
private void OnStateChanged()
{
    if (StateChanged != null)
    {
        StateChanged(this, EventArgs.Empty);
    }
}

 

Вся "фишка" здесь, понятное дело, в многопоточности: во втором случае подписчик (в другом потоке) может отписаться от события как раз в тот момент, когда проверка на null уже прошла, но перед тем, как будет произведена попытка возбудить событие. Соответственно, второй случай - потенциальный кандидат на NullReferenceException, к тому же трудно воспроизводимый. В то же время в случае первого примера возникновение такого исключения невозможно.

Теперь вернемся к коллекциям и посмотрим на них с этой стороны.
Допустим, у нас есть какой-либо объект, который "выставляет наружу" коллекцию (сейчас нам не важно, какого типа, массив ли, IEnumerable ли, пусть это будет List<> для примера):

 

public sealed class TestObject
{
    public List<string> Users { get; set; }
 
    public void AddUser(string user)
    {
        Users.Add(user);
    }
}

 

Предположим, что есть некий код, работающий с экземпляром этого объекта. Допустим, что этот код зачем-то перебирает список пользователей, который содержится в объекте, с помощью foreach, такое часто бывает. А в это время, в другом потоке, приходит команда на добавление нового пользователя в список с помощью, например, метода AddUser(...). В этом случае мы немедленно получаем исключение в том коде, который занимался перебором значений. Исключение будет связано с тем, что коллекция была изменена и дальнейший перебор ее невозможен. Кстати, узнать из исключения кто, где и зачем поменял коллекцию будет невозможно, что тоже не сахар в смысле отладки.

Хорошо, будем перебирать коллекцию не с помощью foreach, а с помощью for, в этом случае от такого рода исключения мы еще как-то застрахуемся:

 

for (int i=0;i<to.Users.Count;i++)
{
    Console.WriteLine(to.Users[i]);
    Thread.Sleep(500);
}

 

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

В общем, думаю, проблема понятна. Что с решением?

В общем виде решение, очевидно, нужно искать в неизменяемости значений, то, что называется Immutable object.
Immutable object не может изменить своего значения после того, как он единожды был инициализирован. Пример - тип String в .NET. К слову, в .NET нет возможности объявить объект константным (неизменяемым), к большому сожалению. Поэтому приходится "выкручиваться" на предмет immutable objects по-всякому. Об этом, возможно, в следующий раз.

Что может дать такая неизменяемость? На самом деле очень многое. Такие объекты можно без опасений использовать из нескольких потоков, не беспокоясь о том, что их состояние вдруг изменится, не переживая по поводу "грязного чтения" (это когда в одном потоке вы уже успели изменить 5 свойств объекта, а еще 15 на очереди, когда кто-то начинает работать с тем же объектом в другом потоке, получая, по сути, невалидный объект) не заморачиваясь излишней синхронизацией... На самом деле, представьте, сколько придется писать кода для синхронизации потоков, всяческих lock'ов, для того, чтобы вышеуказанный простой пример стал потокобезопасным, надежным? А ведь надо еще не запутаться в этих локах, не допустить мертвых блокировок.. А если что-то посложнее?

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

Первое. Использовать неизменяемые коллекции. При этом возвращение из свойства "стандартной" ReadOnlyCollection<> (в виде "return m_Users.AsReadOnly();")  нам, в общем случае, не подойдет, так как она не имеет своего енумератора и является всего лишь оберткой над List<>, изменив который мы получим все те же проблемы со "сломанным" енумератором. Массив нам не подойдет тоже, так как позволяет изменять элементы используя индексатор.
Получается, что придется делать собственную реализацию, скажем, ReadOnlyList<T> : IList<T>, который не позволит изменять коллекцию вообще.

Второе. Вытекает из первого. Так, как мы собираемся хранить в нашем TestObject не List<T>, а ReadOnlyList<T>, то при любой необходимости изменения коллекции мы просто создаем новый ReadOnlyList на основе имеющегося+все необходимые изменения, а потом просто подменяем им существующий. Подмена ссылки - операция атомарная, поэтому "грязного чтения" не возникнет.

Соблюдя эти два простых условия мы получим следующую простую ситуацию: те, кто уже получил ссылку на коллекцию из свойства Users, так и продолжат работать с ней, с той, которая была валидна на момент обращения к свойству, даже если само свойство Users и поменяет свое значение и станет возвращать уже другую коллекцию. И никто не умрёт :)
За исключением, однако, случая из последнего примера (цикл for). Здесь ошибки все еще возможны, так как обращение к свойству Users производится дважды (на каждую итерацию): в момент проверки условия и в момент обращения к элементу. Между двумя этими моментами Users может поменять свое значение - и "привет".
От этого последнего неудобства можно избавиться аж двумя способами:

  1. Тем же способом, как и в случае с событием, о котором я говорил в самом начале: сначала получить ссылку на экземпляр, а потом уже делать с ним все, что хочешь и енумерировать сколь угодно долго любыми циклами. Однако, это неудобно: не заставишь же разработчиков не использовать некоторых конструкций. Поэтому, смотрим пукнт 2.
  2. Возвращать IEnumerable<> :) Во-первых это и означает получение ссылки и дальнейшую работу только с ней, а во-вторых, for'ом по такому свойству пройтись просто не получится :) Если кому-то и нужно использовать именно конструкцию for, или просто нужен массив, или лист, то, опять же, создать его на основе IEnumerable<> не представляет никакой сложности. И, главное, это не скажется на работе других частей приложения.

Меньше локов, товарищи ;)

Powered by BlogEngine.NET 1.6.0.0

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter


Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010

Sign in