Вот в комментариях к прошлому постингу был интересный вопрос о том, что не оставить ли все "заморочки" с многопоточностью на совести программиста?
Действительно, с одной стороны мысль разумная. С другой же стороны речь идет именно о гарантиях. Ну, как пример возьмем тот же самый объект:
| public sealed class TestObject |
| { |
| public IEnumerable<string> Users { get; set; } |
| } |
Какого рода действия здесь может предпринять программист для того, чтобы чувствовать себя уверенно в многопоточной среде?
Самое простое и часто распространенное - синхронизация. При этом, понятное дело, нужно синхронизировать как операции чтения, так и операции записи. То есть, если коллекцию кто-то пишет, то ее нельзя читать. А если коллекцию кто-то читает, то ее нельзя писать. Тогда случае самого примитивного lock'а мы сводим "на нет" всю нашу многопоточность: одновременно с коллекцией может работать только один поток, тот, который захватил этот самый монитор (lock).
Можно придумать более хитроумный подход, когда мы позволяем читать коллекцию многопоточно, а блокировать коллекцию будем только в случае ее изменения. Естественно, до начала внесения изменений все, кто читал коллекцию должны завершить свои операции (те, кто еще не начал - ждут), затем мы вносим изменения, а затем снова разрешаем операции чтения.
Подобные механизмы обычно реализуются через две очереди, одна содержит операции чтения и задачи из нее выполняются во многих потоках, вторая - операции записи, задачи из которой выполняются эксклюзивно. Такой подход легко реализуется через, скажем, CCR, а так же специальная инфраструктура для этого имеется в свободно распространяемой библиотеке Wintellect Power Threads, написанной Джефри Рихтером.
Однако и в случае этого подхода нас ожидает ряд сюрпризов. Один из них - продолжительность операции чтения коллекции. Представьте, что енумерируя TestObject.Users я для каждого элемента коллекции делаю запрос к веб-сервису. Допустим, я получаю данные по пользователю, обрабатываю их, вношу изменения в соответствующие статистические данные, делаю биллинг и т.д. Операции-то небыстрые. Я енумерирую, а все остальные операции просто стоят и ждут. Потому, что следующая на очереди - операция изменения коллекции. Она ждет, пока я закончу читать, остальные операции чтения ждут, пока операция записи выполнит свою работу эксклюзивно. Семеро одного ждут.
Да, я могу вместо енумерирования Users каждый раз создавать на его основе новую коллекцию, или лист, и потом тормозить с ней столько времени, сколько хочу, не мешая другим. Но - я вынужден буду делать это каждый раз, когда я хочу работать с коллекцией Users. Чтобы не заставлять ждать других, я каждый раз при чтении создаю новую копию коллекции. Плюс мне, человеку, в общем-то постороннему, нужно знать и помнить, что работать надо так и только так. Плюс - достаточно сложная инфраструктура с задачами, очередями... Точнее, минусы все это, в общем-то ;)
Поэтому, в очень многих случаях, я считаю вполне оправданным создание новой коллекции каждый раз при ее изменении. Многие случаи - это тогда, когда операций чтения существенно больше, чем операций записи. Что в основном и случается.