У нас есть ArrayPool для переиспользования массивов. Работает это так: взяли массив определенной длинны что то с ним поделали и положили обратно. Нужно это для больших объектов которые по логике программы долго не должны храниться. В предыдущей статье описана эта проблема.

Так же могут понадобиться не только массивы, поэтому попробуем написать универсальный пул. Заранее скажу что есть готовые решения.

Нам понадобится хранилище и методы для создания и очистки элемента в хранилище.

    public sealed class Pool<T>
    {
        readonly List<T> _items = new List<T>();
        readonly Func<T> _itemCreator = null;
        readonly Action<T> _itemClearer = null;
        readonly int _maxItems;
        SpinLock _lock;

        public Pool(Func<T> itemCreator, int maxItems)
        {
            _itemCreator = itemCreator
                ?? throw new ArgumentNullException(nameof(itemCreator));

            _maxItems = maxItems;
        }

        public Pool(Func<T> itemCreator, Action<T> itemClearer, int maxItems)
            : this(itemCreator, maxItems)
        {
            _itemClearer = itemClearer
                ?? throw new ArgumentNullException(nameof(itemClearer));
        }

        public Pooled Rent()
        {
            T rented;
            bool lockTaken = false;
            try
            {
                _lock.Enter(ref lockTaken);

                if (_items.Any())
                {
                    rented = _items[0];
                    _items.RemoveAt(0);
                }
                else
                    rented = _itemCreator();
            }
            finally
            {
                if (lockTaken)
                    _lock.Exit();
            }

            return new Pooled(rented, this);
        }

        public int Count => _items.Count;

        public class Pooled : IDisposable
        {
            readonly Pool<T> _pool;
            public readonly T item;
            public Pooled(T item, Pool<T> pool)
            {
                this.item = item;
                _pool = pool;
            }

            bool disposedValue = false;
            public void Dispose()
            {
                bool lockTaken = false;
                try
                {
                    _pool._lock.Enter(ref lockTaken);

                    var dValue = Volatile.Read(ref disposedValue);
                    if (!dValue)
                    {
                        Volatile.Write(ref disposedValue, true);

                        if (_pool.Count < _pool._maxItems)
                        {
                            _pool._itemClearer?.Invoke(item);
                            _pool._items.Add(item);
                        }
                    }
                }
                finally
                {
                    if (lockTaken)
                        _pool._lock.Exit();
                }
            }
        }
    }


Метод Rent берет элемент обернутый в Pooled или создает его, а возврат произойдет в Dispose.

Конструктор с одним параметром означает что элементы не будут очищаться.

Приведу пример для List:
        var bigListPool = new Pool<List<long>>(Creator, Clearer, 10);

        using (var pooledList = bigListPool.Rent())
        {
            var list = pooledList.item;
        }


Создание и очистка List:
        List<long> Creator() => new List<long>(1024 * 1024);

        void Clearer(List<long> l) => l.ForEach(i => i = 0);


Если список другого размера то надо создать еще один пул. Далее можно все это дело обернуть и внедрить синглтоном через ConfigureServices.

Проект

Комментарии (129)


  1. AgentFire
    20.05.2019 23:52
    +2

    1. itemCreator = itemCreator ?? throw new ArgumentNullException("itemCreator");
      Если уж использовать фишки шарпа, то не стоит забывать и про nameof
    2. Count в данном случае больше подходит в виде свойства, т.к. не производит никаких вычислений.
    3. Для удобства, а так же исключения такой фигни как try-finally, возможно, стоит создать IDisposable обертку:
      using (var pooled = pool.Rent())
      {
      // работем с pooled.Item
      } // Автовозвращение
    4. Поле _items не изменяется, стоит задуматься о модификаторе readonly.


    1. AgentFire
      21.05.2019 22:12

      Я смотрю, пул был немного доработан. Вот, что я об этом думаю:


      1. SpinLock. Разработан для ситуаций, когда используется быстрая или очень быстрая работы системы, когда важна производительность. Следует ли задуматься о создании метода-перегрузки для медленной работы, оно же — асинхронной?
        await pool.Return(pooled.Item, token); // Белиссимо!
      2. SpinLock. Судя по этой статье, он использует тот же Monitor, из чего следует, что удобнее и проще его заменить на уже готовый синтактический сахар в виде блока lock().
      3. Метод Return весьма неоднозначно определяет для конечного пользователя, будет ли возвращенный объект "деструктурирован". В данном случае у тебя не вызывается _itemClearer, если достигнута верхняя граница. Стоит подумать об однозначности и понятности метода Return. Как вариант, использовать bool (Try)Return, возвращая пользователю информацию о том, будет ли объект переиспользован, или же он будет собран сборщиком, как лишний.
      4. Домашнее задание на дом — рассмотреть все плюсы и минусы замены класса Pooled<T> на структуру, в контексте высокопроизводительного приложения, коим ты наградил свое изобретение, и подвести итог, что же все-таки лучше. Сам не знаю эту часть настолько глубоко.


      1. retran
        21.05.2019 22:31

        Простите, а что по вашему такое SpinLock?


        1. AgentFire
          21.05.2019 22:35

          вах, виноват, не использует он Monitor, не прокатило, вычеркиваем!


          1. retran
            21.05.2019 22:41
            +1

            Нет, это мьютекс использует внутри спинлок.

            И, да, тут надо написать «не используйте спинлоки если точно не понимаете что и зачем делаете, используйте lock { }, там уже за вас подумали, а если понимаете — то скорее всего понимаете как написать пул на CAS без блокировок».


            1. AgentFire
              21.05.2019 23:55

              mutex использует winapi, не?


              1. retran
                22.05.2019 02:31

                И Monitor, и Mutex — мьютексы (и спинлок — тоже, но как-то принято их не смешивать с "обычными"). Мне, конечно, стоило уточнить, что я имею в виду Monitor во избежание путаницы, прошу прощения.


                1. Ascar Автор
                  22.05.2019 10:49

                  SpinLock блокирует на пользовательском уровне, зацикливанием.


                  1. retran
                    22.05.2019 12:25

                    А Monitor?


                    1. Ascar Автор
                      22.05.2019 13:17

                      Гибрид


            1. lair
              22.05.2019 00:32

              Дадада, сначала давайте возьмем конкурентную коллекцию, а потом давайте обернем обращение к ней в блокировку. Круто же, чего.


      1. Ascar Автор
        22.05.2019 10:24

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

        3 — ок.
        4 — структуру вместе с using не получится, компилятор делает защитную копию.


        1. lair
          22.05.2019 11:15

          блокировка на время проверки, очистки и добавки в коллекцию

          То есть на самую длительную подконтрольную вам операцию, да. На этом фоне непонятно, зачем вам вообще ConcurrentBag.


  1. lair
    21.05.2019 00:27

    Ну то есть нам понадобился объект ненадолго, мы его создали, "вернули" в пул, и он теперь там лежит до морковкиного заговения и ест ресурсы, да?


    Ну вы бы хотя бы слабые ссылки прикрутили, что ли.


    (это не вдаваясь в те детали, что за исключением случаев, когда вы очень хорошо понимаете, что вы делаете, писать свой пул не надо)


    1. Ascar Автор
      21.05.2019 00:41

      Если дополнительно оборачивать через WeakReference, то после очередной уборки 2 го поколения пул будет состоять из пустых оберток, и придется создавать опять требуемый набор больших объектов. Нужно чтобы они не очищались для поддержания производительности.

      и он теперь там лежит до морковкиного заговения и ест ресурсы, да?

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


      1. lair
        21.05.2019 00:51

        после очередной уборки 2 го поколения пул будет состоять из пустых оберток

        Если у вас случился уборка второго поколения, то может это и к лучшему.


        придется создавать опять требуемый набор больших объектов

        Зато между моментом GC и следующим использованием объекта у приложения будет больше доступной памяти (и меньше ее фрагментация).


        Нужно чтобы они не очищались для поддержания производительности.

        … поэтому давайте забьем LOH, чтобы его нужно было чаще очищать, чтобы GC сильнее тормозил приложение.


        Как бы подразумевается что эти объекты используются постоянно.

        Вот это как раз ошибочное "подразумевается". Для массивов-то оно понятно, массивы — это буфера, они часто нужны, и их можно подобрать одного переиспользуемого размера. А вот какие еще объекты вам так часто надо переиспользовать, что вы для этого сделали пул, и получили гарантированный измеримый выигрыш в используемых ресурсах?


        Более того, что вообще у вас за объекты такие, которые попадают в LOH?


        1. Ascar Автор
          21.05.2019 01:01

          Если у вас случился уборка второго поколения, то может это и к лучшему.

          Будут удалены объекты пула которые еще необходимы, вы ведь не сможете определить нужны они или нет.
          Зато между моментом GC и следующим использованием объекта у приложения будет больше доступной памяти (и меньше ее фрагментация).

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

          Так GC квоты сам определяет, он умный.
          Вот это как раз ошибочное «подразумевается». Для массивов-то оно понятно, массивы — это буфера, они часто нужны, и их можно подобрать одного переиспользуемого размера. А вот какие еще объекты вам так часто надо переиспользовать, что вы для этого сделали пул, и получили гарантированный измеримый выигрыш в используемых ресурсах?

          Людям из предыдущего поста понадобилось. Собственно поэтому я и задумался о пуле.


          1. lair
            21.05.2019 01:11

            Будут удалены объекты пула которые еще необходимы

            Что значит "необходимы"?


            вот и будет объект по новой создаваться и убираться уборщиком.

            Ну и прекрасно же.


            Так GC квоты сам определяет, он умный.

            Вот именно. Он умный, но вы считаете, что умнее его, и создаете пул, который мешает ему делать свою работу. Не надо так.


            Людям из предыдущего поста понадобилось.

            Им как раз не понадобилось, они решили, что переиспользование им не выгодно. Им понадобилось уменьшить нагрузку на LOH, и они это успешно сделали.


            А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?


            1. Ascar Автор
              21.05.2019 01:32

              Что значит «необходимы»?

              Значит этот объект в скором времени будет запрошен.
              Ну и прекрасно же.

              Возвращаемся к проблеме которая и была. Это не прекрасно…
              Вот именно. Он умный, но вы считаете, что умнее его, и создаете пул, который мешает ему делать свою работу. Не надо так.

              Как раз не считаю, и создаю долгоживущие большие объекты, все по логике GC.
              Им как раз не понадобилось, они решили, что переиспользование им не выгодно. Им понадобилось уменьшить нагрузку на LOH, и они это успешно сделали.

              А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?

              Там большой объект просто разбит на маленькие. Если бы GC считал что это норма, то и клал бы все в 0е поколение. То решение работает, но мне по аналогии ArrayPool кажется правильнее делать через пул.
              А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?

              Например List. У нас же не одни только массивы существуют.


              1. lair
                21.05.2019 01:37

                Значит этот объект в скором времени будет запрошен.

                Вы не можете знать, будет ли он в скором времени запрошен.


                Возвращаемся к проблеме которая и была.

                А была ли?


                Там большой объект просто разбит на маленькие.

                И это успешно решило проблему.


                То решение работает, но мне по аналогии ArrayPool кажется правильнее делать через пул.

                Вам кажется. У вас нет работающего решения, которое было бы измерено под нагрузкой.


                Например List. У нас же не одни только массивы существуют.
                Ответить

                List — это не сценарий. Я спрашиваю про конкретный сценарий использования, где у вас есть просадка в производительности, которую вы успешно решили за счет использования вашего пула объектов (в данном случае — списков).


                1. Ascar Автор
                  21.05.2019 01:57

                  Вы не можете знать, будет ли он в скором времени запрошен.

                  Если создается пул значит эти объекты должны будут часто использоваться иначе его не надо создавать.

                  А была ли?

                  Да, большой логически коротко живущий объект.

                  И это успешно решило проблему.

                  Я описываю свой подход в этой статье и ни с кем не спорю по реализациям той статьи.

                  Вам кажется. У вас нет работающего решения, которое было бы измерено под нагрузкой.

                  Решения работающего с этим классом нет, поэтому то я и делюсь идеями.
                  List — это не сценарий. Я спрашиваю про конкретный сценарий использования, где у вас есть просадка в производительности, которую вы успешно решили за счет использования вашего пула объектов (в данном случае — списков).


                  В этой статье я и не опираюсь на происходящее до этого реальное использование.


                  1. lair
                    21.05.2019 12:09

                    Если создается пул значит эти объекты должны будут часто использоваться иначе его не надо создавать.

                    "Должны" — не эквивалентно "будут". Что хуже, вы не знаете, сколько объектов вам надо, но при этом создаете их неограниченное количество.


                    Да, большой логически коротко живущий объект.

                    Это не проблема, это что-то придуманное. Проблема — это когда вы нашли и измерили. А вы, по вашим же заверениям, этого не сделали.


                    Я описываю свой подход в этой статье

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


                    Решения работающего с этим классом нет, поэтому то я и делюсь идеями.
                    В этой статье я и не опираюсь на происходящее до этого реальное использование.

                    Вот не надо так делать с ресурсными оптимизациями, вы не знаете ни что вам в реальности будет нужно, ни какие там проблемы, ни какие ограничения. Сначала нагрузка и измерения, потом решение.


                    1. Ascar Автор
                      21.05.2019 12:32

                      «Должны» — не эквивалентно «будут». Что хуже, вы не знаете, сколько объектов вам надо, но при этом создаете их неограниченное количество.

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

                      Эта проблема описана не мной, но я предлагаю альтернативное решение.
                      хотя ваш подход ее не решает.

                      Ее вполне может решить. Я же не просто так обратил внимание на ArrayPool. До этого провел проверку, большие объекты без него создаются у удаляются, с ним только 4, на 4х ядерном процессоре.
                      вы не знаете ни что вам в реальности будет нужно, ни какие там проблемы, ни какие ограничения

                      Почему не знаю? Вполне себе представляю. Синтетический пример на net core mvc после прочтения той статьи запускал и проблема проглядывалась.


                      1. lair
                        21.05.2019 12:52

                        Их число соответствует числу ядер процессора по понятной причине.

                        Ээээ, какой такой "понятной причине"?


                        Эта проблема описана не мной, но я предлагаю альтернативное решение.

                        Если эта проблема описана — покажите описание. Пока его нет.


                        Ее вполне может решить.

                        Авторы статьи с вами не были согласны. Продемонстрируйте на испытаниях.


                        Почему не знаю? Вполне себе представляю.

                        Именно потому, что "представляете", а не знаете. Но проблема еще не возникла, возможно, не возникнет, или возникнет вообще не там. Я как-то долго и больно отлаживал перформанс десериализации XML, только чтобы выяснить, что ошибка была в одном неудачном try...catch (а совсем не там, где я думал, а думал я как раз на утечку памяти).


                        1. Ascar Автор
                          21.05.2019 13:15

                          Ээээ, какой такой «понятной причине»?

                          Первый объект создается когда хранилище пусто. Остальные — когда первый занял один объект, и одновременно оставшиеся потоки его запросят. Конечно при асинхронном занятии объектов из хранилища возможно больше экземпляров, вопрос в том правильная ли такая асинхронная логика.
                          Если эта проблема описана — покажите описание. Пока его нет.

                          Так уже не раз упомянулась из той же статьи. Или вы про что?
                          Авторы статьи с вами не были согласны. Продемонстрируйте на испытаниях.

                          Я не смогу продемонстрировать на их примере по понятным причинам, синтетический пример я проверял. Напишу еще раз: без пула множественное расположение в LOH и очистка, с ним — только 4 экземпляра.
                          Именно потому, что «представляете», а не знаете.

                          Паттерн «пул» давно придумали для решения описанной проблемы. Я предложил его имплементацию.
                          Но проблема еще не возникла, возможно, не возникнет, или возникнет вообще не там.

                          Пока гром не грянет, мужик не перекрестится

                          Я не смотрю так: не нужно тебе что то именно сейчас — не пиши.


                          1. lair
                            21.05.2019 13:21

                            Первый объект создается когда хранилище пусто. Остальные — когда первый занял один объект, и одновременно оставшиеся потоки его запросят.

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


                            Так уже не раз упомянулась из той же статьи. Или вы про что?

                            Проблема из той статьи — не ваша, и ваше решение к ней не подходит.


                            Напишу еще раз: без пула множественное расположение в LOH и очистка, с ним — только 4 экземпляра.

                            А синтетическая производительность?


                            Паттерн «пул» давно придумали для решения описанной проблемы.

                            Паттерн "пул", как и другие паттерны, предполагает формализованную задачу.


                            Я не смотрю так: не нужно тебе что то именно сейчас — не пиши.

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


                            1. Ascar Автор
                              21.05.2019 13:32
                              -1

                              Во-первых, у вас может быть больше одновременных потоков, чем процессоров.

                              Потоки могут выполняться одновременно ограничиваясь числом ядер. 4 ядра — 4 контекста потока выполняются одновременно в ОС.
                              один раз за время жизни приложения случился пик

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

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

                              Так я не настаиваю, тут мы обсуждаем идеи.


                              1. lair
                                21.05.2019 13:48

                                Потоки могут выполняться одновременно ограничиваясь числом ядер. 4 ядра — 4 контекста потока выполняются одновременно в ОС.

                                Если у вас есть такое ограничение на входе. Его может и не быть.


                                надо думать о освобождении удалении объектов при снижении нагрузки

                                Вот, об этом и речь.


                                Реализация пула может быть применима к чему угодно.

                                Применить можно что угодно к чему угодно, вопрос в том, есть ли выигрыш.


                                Так я не настаиваю, тут мы обсуждаем идеи.

                                В перформансе намного осмысленнее обсуждать измерения. Идеи без измерений ничего не стоят.


                                1. Ascar Автор
                                  21.05.2019 13:55
                                  -1

                                  Если у вас есть такое ограничение на входе. Его может и не быть.

                                  Это принцип работы ОС. Допустим у вас 1000 потоков в одном процессе, но процессор имеет 4 ядра. Поэтому ОС сможет только переключатся в 4 контекста одновременно. Одновременно могут быть запрошены 4 объекта из хранилища.
                                  Вот, об этом и речь.

                                  Я рад что с N — го раза вы вчитались в мой текст.
                                  Применить можно что угодно к чему угодно, вопрос в том, есть ли выигрыш.

                                  На усмотрение потребителю.
                                  В перформансе намного осмысленнее обсуждать измерения. Идеи без измерений ничего не стоят.

                                  Элементарный тестовый пример с циклом в котом создаются объекты, и пример где используется пул. Пока это все что есть.


                                  1. lair
                                    21.05.2019 14:01

                                    Поэтому ОС сможет только переключатся в 4 контекста одновременно. Одновременно могут быть запрошены 4 объекта из хранилища.

                                    Вам не надо "одновременно", вам надо "до момента, как объекты отпущены". И если у вас тысяча потоков, то у вас может быть тысяча запрошенных объектов подряд.


                                    На усмотрение потребителю.

                                    Это называется "вы не знаете". О чем и речь.


                                    Элементарный тестовый пример с циклом в котом создаются объекты, и пример где используется пул. Пока это все что есть.

                                    То есть ничего совпадающего с реальной нагрузкой.


                                    1. Ascar Автор
                                      21.05.2019 14:08

                                      Вам не надо «одновременно», вам надо «до момента, как объекты отпущены». И если у вас тысяча потоков, то у вас может быть тысяча запрошенных объектов подряд.

                                      Так я и писал про такой вариант развития. Надо думать про то как померить нагрузку и почистить пул.
                                      Это называется «вы не знаете». О чем и речь.

                                      Ну я не могу знать где будет использоваться библиотека, могу только описать ее и то что она делает, остальное на усмотрение.
                                      То есть ничего совпадающего с реальной нагрузкой.

                                      Ну реальная нагрузка сводится к этому, много созданий, много очисток. Я уже понял вашу логику: «пиши только о том с чем столкнулся в реальном проекте».


                                      1. lair
                                        21.05.2019 14:11

                                        Надо думать про то как померить нагрузку и почистить пул.

                                        Или сделать банальную верхнюю границу.


                                        Ну реальная нагрузка сводится к этому, много созданий, много очисток.

                                        … и совершенно не обязательно, что пул даст выигрыш. О чем и речь.


                                        1. Ascar Автор
                                          21.05.2019 14:14

                                          Или сделать банальную верхнюю границу.

                                          Это уже конструктивнее.
                                          … и совершенно не обязательно, что пул даст выигрыш. О чем и речь.

                                          При определенных условиях, конечно не обязательно. Каждый инструмент для своей задачи.


                                          1. lair
                                            21.05.2019 14:15

                                            При определенных условиях, конечно не обязательно.

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


                                            1. Ascar Автор
                                              21.05.2019 14:18

                                              Цикл с пулом и без. Возможно дополню статью примером.


                                              1. lair
                                                21.05.2019 14:21

                                                Цифры в студию, включая потребление памяти.


                                                1. Ascar Автор
                                                  21.05.2019 14:37

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


                                      1. retran
                                        21.05.2019 14:56
                                        +1

                                        Ну реальная нагрузка сводится к этому, много созданий, много очисток.


                                        Вы уверены, что «много созданий, много очисток» в clr медленнее пула в общем случае?


                                        1. Ascar Автор
                                          21.05.2019 15:11

                                          Речь про большой объект, который в LOH упадет. Без пула он логически одноразовый но будет лежать до очистки, соответственно по достижении заданной квоты они будут очищаться(вместе со всей кучей), а это значит что все потоки приостановятся и это займет время. Если объект переиспользуемый то не будет множества его одноразовых копий. Как то так.

                                          В общем случае GC быстро чистит и двигает указатели, там вроде бы даже есть фоновый режим.


                                          1. retran
                                            21.05.2019 19:06

                                            Я не могу представить себе реальный сценарий.
                                            Такой большой объект — это или очень большой массив, или массив структур больших, или буфер. Т. е. не те штуки на инициализации которых обычно можно сэкономить. Если это синглтон, то почему бы ему просто не быть синглтоном? Не понимаю.


                                            1. Ascar Автор
                                              21.05.2019 19:41

                                              Если синглтон несколько задач возьмет придется его блокировать.


                                              1. retran
                                                21.05.2019 20:03

                                                Так вы для начала определитесь какую именно задачу вы решаете. Один объект на таск? Один объект на поток? Произвольное количество объектов на произвольное количество задач на произвольном количестве потоков?


                                                1. Ascar Автор
                                                  21.05.2019 20:07

                                                  Вообщем: реализую пул. Тем примером просто проверил его возможности.


                              1. retran
                                21.05.2019 19:55

                                Потоки могут выполняться одновременно ограничиваясь числом ядер. 4 ядра — 4 контекста потока выполняются одновременно в ОС.


                                В вашем примере количество потоков ограничено не количеством процессоров, а размером пула потоков. Добавьте в ваши таски какой-нибудь Thread.Sleep(1000) и вы начнете получать случайное количество созданных объектов в пуле большее, чем количество процессоров.


                                1. Ascar Автор
                                  21.05.2019 19:57

                                  Это понятно, с await Task.Delay(1000) щас посмотрел. Верхнюю границу сделал кстати. Thread.Sleep может задедлочить.


                                  1. retran
                                    21.05.2019 20:02

                                    Thread.Sleep может задедлочить.


                                    В каком месте? Thread.Sleep нужен чтобы гарантированно заблокировать поток, иначе количество взятых из пула потоков может и не превысить количество процессоров.


                                    1. Ascar Автор
                                      21.05.2019 20:06

                                      del


                                      1. retran
                                        21.05.2019 20:08

                                        Проверил, дедлоков нет. Стоит подождать подольше.


                                        1. Ascar Автор
                                          21.05.2019 20:10

                                          Так, я вру, у меня там число итераций огромное просто.


                                          1. retran
                                            21.05.2019 20:12

                                            Давайте уточним на всякий случай — что значит «половина задедлочилась»?


                                            1. Ascar Автор
                                              21.05.2019 20:15

                                              Смотрите, щас с await Task.Delay все сработало, число итераций было большое, пул заполнен до верхней границы как и ожидалось.
                                              Пример


                      1. retran
                        21.05.2019 19:14

                        Их число соответствует числу ядер процессора по понятной причине. Вы пример из исходников не смотрели.


                        Почитал пример из исходников. В этом примере пул не нужен, можно просто лениво класть ваш объект в ThreadLocal (на самом деле ConcurrentBag в вашей реализации ровно так и сделает).


                        1. retran
                          21.05.2019 19:26

                          Переписал ваш пример — gist.github.com/retran/0f47c5c239ddef876e3674b641da7574


                          1. Ascar Автор
                            21.05.2019 19:49

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


                            1. retran
                              21.05.2019 19:59

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


                              1. Ascar Автор
                                21.05.2019 20:02

                                А потом что с ним происходит?


                                1. retran
                                  21.05.2019 20:07

                                  С кем?


                                  1. Ascar Автор
                                    21.05.2019 20:08

                                    Ну с объектом. Идея его переиспользовать же.


                                    1. retran
                                      21.05.2019 20:11

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

                                      UPD… при произвольном количестве тасков. Это вполне может иметь смысл, если вы хотите что-то пооптимизировать за счет уменьшения contention между ядрами процессора.


                                      1. Ascar Автор
                                        21.05.2019 20:21

                                        Крайний вариант это множественные таски забирают объекты и работают с ними.


                                        1. retran
                                          21.05.2019 20:28

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

                                          UPD общий пул объектов из LOH


                                          1. Ascar Автор
                                            21.05.2019 20:34

                                            если для множественных тасок понадобился общий пул, то где-то что-то не так

                                            Согласен, я про это уже писал. Но как вариант должно и так работать.
                                            Хочется более реальный сценарий.

                                            В комментах поищите (не моих), где то было.


                                            1. retran
                                              21.05.2019 20:41

                                              В комментах поищите

                                              Мне лень, извините ?\_(?)_/?


                                              1. Ascar Автор
                                                21.05.2019 20:43

                                                Со StringBuilder и ваш коммент.


                                                1. retran
                                                  21.05.2019 21:23

                                                  Там нет ни гигантских обьектов. В моем случае даже конкарренси нет и пул живёт строго в одном потоке.


                                                  1. Ascar Автор
                                                    21.05.2019 21:27

                                                    Тогда и синглтон подходит.


            1. alhimik45
              21.05.2019 12:23
              +2

              А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?

              Например, пул инстансов Хрома. Создавать его на каждую пришедшую задачу слишком долго, поэтому у нас реализован пул хромов, из которых демон берет первый свободный, рендерит всё, что надо, а потом возвращает его. Такая схема позволяет переживать кратковременные всплески нагрузки без увеличения latency, ценой лишней съедаемой памяти после всплеска (до следующего перезапуска демона при балансировке).


              1. lair
                21.05.2019 12:48

                О, спасибо за пример.


                Любопытства ради — а у вас инстансы хрома живут в том же процессе, что и пул?..


                1. alhimik45
                  21.05.2019 12:58

                  Нет, он через Process.Start запускается.


                  1. lair
                    21.05.2019 13:02

                    Ну то есть у вас для очистки памяти надо целиком инстанс перезагрузить? Или у вас демон умеет при свой перезагрузке убивать процессы?


                    1. alhimik45
                      21.05.2019 13:14

                      Демон умеет. При остановке у пула вызывается Dispose и если внутри него IDisposable, то он всех их диспозит, а обертка над процессом Хрома умеет его убивать.


                      1. lair
                        21.05.2019 13:14

                        Сирот не бывает?


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


                        1. alhimik45
                          21.05.2019 13:48

                          Сирот не бывает?

                          Пока не сталкивались


                          1. lair
                            21.05.2019 13:48

                            Круто.


            1. retran
              21.05.2019 13:19

              А какой сценарий у вас, что вам нужен пул объектов (не массивов), и вы имеете от него измеримый выигрыш?


              Много контролов с очень коротким временем жизни, но сложной инициализацией и простой реинициализацией.

              Игровые объекты в играх, классический пример — проджектайлы разных видов.

              Справидливости ради, в LOH попадать здесь не должно ничего.


              1. lair
                21.05.2019 13:22

                Много контролов с очень коротким временем жизни, но сложной инициализацией и простой реинициализацией.

                А теперь читаем задачу из поста:


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

                Совпало?


                1. Ascar Автор
                  21.05.2019 13:39
                  -1

                  Там тоже время затрачивается как и при уборке 2 го поколения, чтоб снизить так же и эти временные затраты на инициализацию.


                1. retran
                  21.05.2019 14:25

                  А я ничего и не говорил про сценарий автора.


          1. Taritsyn
            21.05.2019 10:51

            А вот какие еще объекты вам так часто надо переиспользовать, что вы для этого сделали пул, и получили гарантированный измеримый выигрыш в используемых ресурсах?

            Первое, что приходит в голову – это переиспользование экземпляров класса StringBuilder.


            1. lair
              21.05.2019 12:10

              Обычно в тех случаях, когда у вас StringBuilder жрет LOH, уже надо переставать конкатенировать строки и начать писать в поток.


              Нет, не всегда. Но часто.


              1. Taritsyn
                21.05.2019 12:30

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


                1. lair
                  21.05.2019 12:50

                  Странно, мне казалось, создание StringBuilder — дешевая операция. Нет?


                  1. Taritsyn
                    21.05.2019 13:07

                    Когда у вас создается очень большое количество экземпляров таких классов, то эта операция становится дорогой.


              1. Taritsyn
                21.05.2019 12:56

                Снижение использования LOH больше относится к случаям, когда мы работаем с большими массивами и в этих случаях нам хватает ArrayPool. А ObjectPool нам нужен для случаев, когда у нас создается много «тяжелых» объектов (например, экземпляров классов StringBuilder и List). Кстати, в реализации пула StringBuilder’ов на основе библиотеки Microsoft.Extensions.ObjectPool емкость удерживаемого в пуле экземпляра класса StringBuilder ограничена 4096 символами.


                1. lair
                  21.05.2019 13:04

                  В чем конкретно у вас тяжесть StringBuilder, и нельзя ли эту проблему решить отказом от него вовсе?


                  (я вполне серьезно спрашиваю, потому что в моих личных сценариях, где много сериализации/десериализации, я обычно иду именно по пути преобразования всего, что можно, в потоковую модель)


                  1. Taritsyn
                    21.05.2019 13:16
                    +2

                    Не всегда можно отказаться от использования StringBuilder‘ов. Например, команда ASP.NET Core не может сделать этого, поэтому они используют пул StringBuilder’ов в ResponseCachingMiddleware.


                    1. lair
                      21.05.2019 13:17

                      Спасибо за пример.


    1. retran
      21.05.2019 13:14

      Ну вы бы хотя бы слабые ссылки прикрутили, что ли.


      Зачем? Типичные сценарии использования пулов предполагают снижение нагрузки на gc и экономию на аллокациях (там где они дорогие) и инициализации за счет поедания памяти.

      Вот только, да, если уж нужен пул, то надо очень аккуратно руками планировать его жизненный цикл, чего у автора нет.


      1. lair
        21.05.2019 13:16

        Чтобы память жрать контролируемо. А то один скачок запросов — и у нас аллоцировано сто экземпляров, которые потом не нужны.


        1. retran
          21.05.2019 13:38

          Это иногда лучше, чем фриз на сборке мусора в самый неподходящий момент. Почистить можно руками, когда мы точно знаем, что скачок закончился (имхо, в тех сценариях где использование пулов разумно — это реально). Ну и обязательное ограничение размера пула сверху тоже никто не отменял (чего у автора, тоже, кстати нету).


          1. lair
            21.05.2019 13:50

            Это иногда лучше, чем фриз на сборке мусора в самый неподходящий момент.

            Главное помнить, что пул может и увеличивать шансы на сборку мусора.


            Ну и обязательное ограничение размера пула сверху тоже никто не отменял (чего у автора, тоже, кстати нету).

            О том и речь, что у автора много чего нету.


      1. Ascar Автор
        21.05.2019 13:18

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


  1. Taritsyn
    21.05.2019 10:46

    Вы пробовали универсальный пул от Microsoft — Microsoft.Extensions.ObjectPool?


  1. lair
    22.05.2019 00:21

    if (!_items.Contains(item))

    Воу-воу, полный перебор пула на каждом возврате элемента? Причем не просто полный перебор, а еще и полное копирование с созданием нового List<T>? От лишних аллокаций хотели избавиться, да?


    1. Ascar Автор
      22.05.2019 10:30

      Только лист, не самих объектов в них. А как вы предлагаете проверять? Без проверки тоже оставлять не хочется. Можно ConcurrentDictionary конечно попробовать. Либо в цикле пройтись. Что плохого в переборе то пула? Там не должно быть много элементов.


      1. lair
        22.05.2019 11:14

        Только лист, не самих объектов в них.

        А это, типа, дешевая с точки зрения аллокации операция (особенно учитывая, что нынешняя реализация не задает capacity для List)?


        А как вы предлагаете проверять?

        Я лично предлагаю не писать велосипедов, пока не стало понятно, в каких условиях они будут использоваться. В данном конкретном случае мне, например, непонятно, зачем эта проверка вообще.


        Что плохого в переборе то пула?

        Аллокации (и сборка мусора), от которых вы, по идее, обещали избавиться, причем в неожиданный для пользователя момент.


        Там не должно быть много элементов.

        Это почему вдруг? У вас максимальное число элементов задается пользователем.


        1. Ascar Автор
          22.05.2019 11:17

          Проверка нужна для защиты от дублирования.


          1. lair
            22.05.2019 11:20

            А откуда у вас возьмется дублирование?


            (вы же понимаете, да, что недобросовестный клиент может ваш пул испортить только в путь?)


            1. Ascar Автор
              22.05.2019 11:35

              Например Dispose продублируется, а флаг закешируется и будет двойной Return. В любом случае наличие дублей означает фатальную неисправность.

              Я не пойму что там будет копироваться при переборе? И при Contains вы говорите о копировании? Не могли бы пояснить этот момент? Размер листа определен в Creator, но это размер объекта пула.


              1. lair
                22.05.2019 12:25

                Например Dispose продублируется

                Например, как? И не проще ли делать защиту от двойного Dispose внутри disposable, а не в Return?


                В любом случае наличие дублей означает фатальную неисправность.

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


                Я не пойму что там будет копироваться при переборе?

                А вы вообще в курсе, как работает Contains на ConcurrentBag?


                1. Ascar Автор
                  22.05.2019 12:43

                  Например, как? И не проще ли делать защиту от двойного Dispose внутри disposable, а не в Return?

                  Там есть проверка, можно volatile добавить, но в Return понадежнее, у нас не только Dispose возвращает, а и сам метод.
                  Тогда вам нужно множество с гарантированной уникальностью

                  ConcurrentDictionary тоже осуществляет проверку. Пул даст выигрыш если его по назначению использовать.
                  А вы вообще в курсе, как работает Contains на ConcurrentBag?

                  Так я у вас поинтересовался, вы сказали, теперь поясните. Про простой перебор в цикле тоже, что там за аллокации.


                  1. lair
                    22.05.2019 13:27

                    в Return понадежнее, у нас не только Dispose возвращает, а и сам метод.

                    … и как у вас Return может быть вызван дважды для одного объекта?


                    ConcurrentDictionary тоже осуществляет проверку.

                    И приносит дополнительные накладные расходы. Собственно, он частный случай "множества с гарантированной уникальностью".


                    Так я у вас поинтересовался, вы сказали, теперь поясните.

                    Ну то есть не знаете. Окей, краткий экскурс в new ConcurrentBag<object>().Contains("t"):


                    1. У ConcurrentBag<T> нет метода Contains. Следовательно что? Правильно, вызывается Enumerable.Contains.


                    2. Поскольку ConcurrentBag<T> не реализует ICollection<T>, происходит банальное:


                      var comparer = EqualityComparer<T>.Default
                      foreach (TSource element in source)
                        if (comparer.Equals(element, value)) return true;
                      return false;

                      Кстати, полезно подумать, почему использование дефолтного компарера — плохо.


                    3. Теперь нас интересует ConcurrentBag<T>.GetEnumerator. В документации написано: "The enumeration represents a moment-in-time snapshot of the contents of the bag". В принципе, этой фразы достаточно, чтобы понять наши проблемы, но давайте посмотрим в исходник, который за вычетом проверок и блокировок сводится к одной интересующей нас строчке: return ToList().GetEnumerator();.


                    4. Посмотрим в ToList:


                      List<T> list = new List<T>();
                      while (проход_по_линкед_листу)
                         list.Add(currentNode.m_value);


                    Теперь понятно, что копируется?


                    Про простой перебор в цикле тоже, что там за аллокации.

                    Простой перебор в цикле чего?


                    1. Ascar Автор
                      22.05.2019 13:56

                      Как и подозревал создается только List и еще некоторые объекты для потокобезопасности, но объекты пула те же из m_headList. За точную инфу конечно спасибо.
                      Этот лист даже если элементов 1000 где по вашему будет размещен в куче?

                      В принципе у меня стоит блокировка, можно теперь и циклом for заменить.


                      1. lair
                        22.05.2019 13:58

                        Этот лист даже если элементов 1000 где по вашему будет размещен в куче?

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


                        Я надеюсь, вы помните, как работает List.Add?


                        В принципе у меня стоит блокировка можно теперь и циклом for заменить.

                        … и что изменится? Кстати, как вы сделаете цикл с for по ConcurrentBag<T>?


                        1. Ascar Автор
                          22.05.2019 14:01

                          Там же, где и (по умолчанию) те объекты, которые вы пытаетесь сложить в пул.

                          Там там то предположительно LOH, а лист в 0 отправится, разница есть.

                          … и что изменится?

                          Как что? Не будет обращение к тем классам по вашей ссылке и удорожания операции.


                          1. lair
                            22.05.2019 14:03

                            Там там то предположительно LOH

                            Нет там никакого "предположительно LOH", все приведенные в этом посте сценарии никакого отношения к LOH не имеют. У вас, напоминаю, универсальный пул.


                            Не будет обращение к тем классам по вашей ссылке и удорожания операции.

                            И как же вам это удастся? Покажите код.


                            1. Ascar Автор
                              22.05.2019 14:08

                              Нет там никакого «предположительно LOH», все приведенные в этом посте сценарии никакого отношения к LOH не имеют. У вас, напоминаю, универсальный пул.

                              Вообще то есть, так же объекты которые долго инициализировать и быстрее обнулить, иначе пул не надо использовать, пишу уже не первый раз вам это…

                              И как же вам это удастся? Покажите код.

                              Как напишу — скину.


                              1. lair
                                22.05.2019 14:16

                                так же объекты которые долго инициализировать и быстрее обнулить

                                А при чем тут LOH? Вон там недалеко приводят в пример StringBuilder, они точно не ложатся в LOH, потому что ограничены 4096 символами.


                                иначе пул не надо использовать

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


                                1. Ascar Автор
                                  22.05.2019 14:27

                                  А при чем тут LOH? Вон там недалеко приводят в пример StringBuilder, они точно не ложатся в LOH, потому что ограничены 4096 символами.

                                  Значит операция по его созданию дорога. Я не вижу никакой проблемы когда короткоживущий List по вашей ссылке будет подчищен GC, все как и с остальными объектами.
                                  Чем дальше мы это обсуждаем, тем больше выясняется, что сценариев, когда ваш пул надо использовать, все меньше и меньше.

                                  Я уже написал для чего это и откуда пришла идея. Как оказалось есть и другие реализации. Вы будите повторять одно и тоже пока с вами не согласятся? По моему это так не работает…


                                  1. lair
                                    22.05.2019 14:30

                                    Значит операция по его созданию дорога.

                                    Дорога аллокация.


                                    Я не вижу никакой проблемы когда короткоживущий List по вашей ссылке будет подчищен GC, все как и с остальными объектами.

                                    А я вот не видел проблемы с тем, чтобы создавать StringBuilder, а у кого-то они есть. Это все говорит нам ровно об одном: нет никакого "общего универсального решения", есть конкретный сценарий, в котором нужно мерять влияние. А у вас нет ни сценария, ни измерений.


                                    Как оказалось есть и другие реализации.

                                    … но вы вместо того, чтобы взять готовую протестированную реализацию, все равно пишете свое, гуляя по всем мыслимым граблям.


                                    1. Ascar Автор
                                      22.05.2019 16:50

                                      … но вы вместо того, чтобы взять готовую протестированную реализацию, все равно пишете свое, гуляя по всем мыслимым граблям.

                                      Я могу что угодно начать писать. Кто угодно может начать писать что угодно. Ловить постоянно бугурты от этого — дело ваше.

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


                                      1. retran
                                        22.05.2019 17:00

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


                                        Проблема в том, что не очень опытный человек вобьет в гугл ключевые слова, найдет вашу статью… и огребет проблем.


                                        1. Ascar Автор
                                          22.05.2019 17:04

                                          Так погуглите. Скорее всего он найдет.


                                          1. retran
                                            22.05.2019 17:07

                                            Так погуглите.


                                            Мне лень. А вы действительно погуглите и разберитесь как оно устроено и почему.


                                            1. Ascar Автор
                                              22.05.2019 17:09

                                              как оно устроено и почему.

                                              Что устроенно? Я же скинул популярное расширение.


                                              1. lair
                                                22.05.2019 22:29

                                                Вот ровно то расширение, которое вы скинули, и устроено. Там есть много полезных ответов, включая вопрос "что делать с двойным возвратом", "как минимизировать блокировки" и так далее.


                                            1. Ascar Автор
                                              22.05.2019 17:10

                                              1. Taritsyn
                                                22.05.2019 21:10

                                                Еще есть хорошая реализация пула объектов из проекта Roslyn, которая не попала в эту выдачу, потому что не представлена в виде NuGet-пакета.


                                      1. lair
                                        22.05.2019 21:38

                                        Я могу что угодно начать писать. Кто угодно может начать писать что угодно.

                                        Тут вопрос не в том, что писать, а в том, что публиковать. Вы, конечно, можете публиковать, что вам пропускают, ну а мы можем комментировать в меру своих желаний.


                      1. lair
                        22.05.2019 14:46
                        +1

                        но в Return понадежнее, у нас не только Dispose возвращает, а и сам метод.

                        Нет-нет, подождите-подождите, давайте все-таки вернемся к вопросу: в какой ситуации у вас Return может быть вызван дважды для одного объекта?


                        1. Ascar Автор
                          22.05.2019 16:19
                          -1

                          Не у меня, у пользователя.


                          1. lair
                            22.05.2019 16:20

                            Нет, не "у пользователя".


                            В каком случае метод Return предлагаемого вами класса может быть вызван дважды, в каком конкретно допустимом сценарии использования?


                            1. Ascar Автор
                              22.05.2019 16:50

                              В допустимом очевидно его один раз надо вызывать.


                              1. lair
                                22.05.2019 21:49

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


    1. lair
      22.05.2019 13:40
      +1

      Кстати, если уж на то пошло, Count — тоже блокирующая операция со стоимостью O(n).


      1. Ascar Автор
        22.05.2019 13:59
        -1

        Он не счетчик там? Если каждый раз считает то не очень это.


        1. lair
          22.05.2019 14:01
          +1

          Ну то есть вы даже не можете посмотреть по уже данной ссылке на исходник класса?


          Нет, он "не счетчик". Да, он каждый раз считает. Под блокировкой.


          1. Ascar Автор
            22.05.2019 14:17
            -2

            Ну то есть вы даже не можете посмотреть по уже данной ссылке на исходник класса?

            Если вы так раздражаетесь, то зачем начинать диалог?


            1. lair
              22.05.2019 14:18
              +1

              Это я еще не раздражаюсь.