Pull to refresh

Comments 16

Конечно пишите.
И ещё предложение, наверное…
Например, там ребята на WCF раз в пять минут рвут и пересоздают коннекты из пула.
Load Balancing the Net.TCP Binding (на биндинге, для которого имеет смысл, конечно).
По крайней мере в моем последнем самописном пуле, безо всякой перебалансировки, коннекты к одной замечательной системе печальным образом протухали, и моя самописка сразу выросла раза в два :\
Тут важно не забывать в CleanUp-е чистить ссылки на другие объекты, иначе будет утекать память.
Вообще складывается ощущение, что сборка мусора порождает у программистов некоторую эйфорию по отношению к выделению-освобождению памяти, и утечки порой находишь не только в сторонних библиотеках (как раз такая утечка из-за нечищеных ссылок в пуле есть в EZ GUI), но и в системных.
В копилку знаний добавлю размышления тов. Rico Mariani на счет пулов:
If you understand the usage patterns of your program well (say it’s a web server) and you can readily predict that the objects you are about to release are going to be needed again in about 2 milliseconds (or something) when the next request comes in, there isn’t a whole lot of point in freeing them. You might just as well hang on to those objects, maybe put them in a pool somewhere, and when that request does come along, you can get them out of the pool.

Sounds great… but there’s a catch. There’s always a catch. Or two.

;)
У вас в коде есть race condition. Т.к. фактическое добавление/удаление объектов не атомарно с изменением счётчика. Так сходу, ничего особо криминального из побочных эффектов назвать не могу. Но как минимум могут быть ложные срабатывания ограничителя: то есть объект в пул ужу вернули, а счётчик ещё не обновился и другой поток не сможет получить экземпляр.

Lock-free это всё круто, но надо ещё учитывать реальную частоту, с которой будут идти обращения к пулу. Вот в вашем, примере идёт работа с сокетами. Любое обращение к сети в 1000 и более раз медленнее, чем время переключения потоков/синхронизации. Так что, если написать такой же пул с обычным List и помечать какой свободен, а какой занят, то реальная производительность приложения вряд ли будет отличаться хотя бы на 1%.
Хотя при этом, конечно, синтетический тест lock-free пул будет быстрее.
Именно поэтому я взял слово «атомарная» в кавычки. Ничего страшного действительно не произойдет.
Поток получит свой объект в таком случае при следующем запросе к пулу.
А что поток будет делать пока не получит объект? У вас что, ещё и каждый поток должен уметь несколько раз попробовать? На мой взгляд это очень не типично для пулов. Пул должен либо вернуть существующий объект, либо создать новый, либо бросить исключение, если по какой-то причине невозможно вернуть объект (кончилась память, достигнут жёсткий лимит на кол-во объектов).

Повторюсь: для данной задачи, синхронизация с блокировками будет тривиальной в реализации, полностью надёжной/корректной, и почти никак не скажется на производительности реального приложения.
Я считаю, что не нужно усложнять. Это из серии микрооптимизаций, вместо X /2 писать X >> 1 :)

При этом, сами по себе синхронные примитивы вроде ConcurrentDictionary мне очень нравятся, они особенно хороши, когда много потоков пишет одновременно. Тогда есть очень большой выигрыш. Если же, пишет 1-2 потока, то обычный Dictionary + ReaderWriterLockSlim работает никак не хуже, а логика работы опять же оказывается проще.
Конечно же ConcurrentDictionary — это не «синхронный примитив», а коллекция, заговорился :)
Нет, поток ничего не должен уметь. Сам пул уходит в ожидание возврата объекта, то есть метод Take гарантированно вернет объект. Суть еще в том, что при адекватном максимальном значении такая ситуация будет редко возникать.

Отвечу на замечания по микрооптимизации. Я согласен с вашим комментарием, в простейшей реализации пула будет достаточно и простого Lock, он хоть и проигрывает в синтетике, но реальное приложение редко будет требовать такого прироста. Моей целью была не реализация наипростейшего пула (на мсдн кстати лежит вполне годная с использованием ConcurrentBag), а демонстрация того, как можно сделать вообще без классических примитивов.
За критику — спасибо.
Интересно, насколько та очевидная реализация быстрее этой.
Думаю использование пула имеет очень ограниченную область применения.
Во-первых, он усложняет приложение.
Во-вторых, он требует очень аккуратного использования. Как уже писали, если вы не очистите ссылки в CleanUp — будет утечка памяти (и это еще в лучшем случае), а сделать такю ошибку — очень легко.
И в-третьих — он бесполезен для больших объектов (те, которые в LOH). И вот почему: откуда может взяться объект размером 85к и больше? Очевидно, что это массив(список, строка). Но есть ли смысл хранить массив в пуле? Ведь массив обычно нужен переменной длины. И как пул может нам помочь здесь? Менять длину массива он не умеет. А если бы даже и умел, то только путем создания нового массива, что сводит на нет преимущество пула. А кроме того, метод CleanUp должен будет чистить весь массив, а это может занять значительное время, если массив действительно большой.
Если же объект не является сам массивом, а содержит поле с массивом, то пул тоже бесполезен, потому что метод CleanUp обязан будет уничтожить ссылку на этот массив. В общем как ни крути, но для больших объектов — пул не подходит в общем случае.
Разумеется, что пулы объектов не нужно пытаться использовать везде. Производительности сборщика мусора, особенно в последней версии фреймворка хватает для большинства случаев.

Для больших массивов есть похожий паттерн — buffer pooling, когда заранее создается большой массив (байтов например), и по требованию выделяются сегменты этого массива.
Попробую ответить на каждый пункт:
1. Мне кажется, что написать Take вместо new, а Release вместо потенциального Dispose — это вполне приемлимое усложение в ситуации, когда нам пул нужен. Если про общую архитектуру идет речь — согласен, +(1-2) класса, хотя это тоже не так много.
2. Ошибку можно сделать и просто «бросая» объекты. Может, сложнее, чем с пулом, не спорю, но тема отдельная))
3. Объекты 85к как правило массивы, вполне могут быть неуправляемым блоком памяти и/или фиксированным буфером для череды каких-либо операций. Операция Array.Clear работает быстрее, чем создание такого же огромного массива.
Посмотрите мой пример, там используется объект SocketAsyncEventArgs, который содержит фиксированный буфер памяти для операции с сокетами. И по тестам видно, что очистка этого буфера происходит быстрее, чем создание нового объекта. Конечно, ограничения использования есть и часть их приведена в самом начале статьи.
Хороших статей очень мало, а они нужны как воздух, спасибо Вам за эту статью, будет здорово, если поделитесь опытом сокет-серверов. Может быть, Вы даже можете подсказать, что делать с такой проблемой — когда есть множество сокет-клиентов, которые должны непрерывно читать данные, а также всегда пересоединяться при обрыве соединения, и этих сокетов может быть очень много. И тут, с одной стороны, не хочется плодить потоки, а с другой — объединять обработку всех сокетов в один поток. Как найти золотую середину?
Использовать асинхронные сокеты и быстрые обработчики событий. При вызове асинхронной операции, она завершается в другом потоке из ThreadPool, который для этого выделяется. Т. е. напрямую потоками управлять не нужно. Их будет использоваться ровно столько, сколько одновременно будет выполняться обработчиков завершения операции. Поэтому обработчики нужно делать быстрыми, чтобы они на долго не занимали временно выделенные под них потоки. Если не получается, то данные можно складывать в очередь и выделять отдельные потоки-задачи для обслуживания этой очереди. Конкретное решение будет зависеть от ваших требований, количества одновременных клиентов, операций и так далее.
Напрямую с потоками работать не надо — проще использовать асинхронную модель .NET (которая поточность разруливает сама через ThreadPool) и методы сокетов Socket.BeginXxx и EndXxx, либо обновленные методы Socket.XxxAsync. Последние как раз ориентированы на работу с пулом переиспользуемых объектов и должны работать быстрее.
Начать можно отсюда, хорошая статья вплоть до методов BeginXxx/EndXxx и про обновленные методы немножко здесь. Также по ссылке из статьи можете глянуть мою реализацию.
Поддерживаю автора в том, что такие вещи нужно пытаться написать. Продолжайте, это хорошая тренировка.

И не поддерживаю конкретно в этой затее.
Т. к. создание объекта в «управляемой памяти» фактически инкремент специального указателя (или нескольких указателей, в случае нескольких потоков), то ускориться в этом месте нельзя.

А вот если объекты «долго создаются», т.е. имеется разбазаривание времени в конструкторе, то этот момент нужно найти и победить.

А насчет пулов…
В одной системе я выкинул чей-то пул коннектов, и добился не только повышения ясности кода и повышения надежности (ради чего и выкидывал), но и увеличения производительности.
В другой системе мне удалось заменить пул одним коннектом к БД — тоже настала нирвана.
Sign up to leave a comment.

Articles