Каково правильное использование ConcurrentBag?

Я уже читал предыдущие вопросы здесь о ConcurrentBag, но не нашел фактического примера реализации в многопоточности.

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

В настоящее время это текущее использование в моем коде (это упрощает не фактические коды):

private void MyMethod()
{
    List<Product> products = GetAllProducts(); // Get list of products
    ConcurrentBag<Product> myBag = new ConcurrentBag<Product>();

    //products were simply added here in the ConcurrentBag to simplify the code
    //actual code process each product before adding in the bag
    Parallel.ForEach(
                products,
                new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
                product => myBag.Add(product));

    ProcessBag(myBag); // method to process each items in the concurrentbag
}

Мои вопросы:
Правильное ли использование ConcurrentBag? Можно ли использовать ConcurrentBag в этом сценарии?

Для меня я думаю, что простой List<Product> и ручной блокировки будут лучше. Причина этого в том, что описанный выше сценарий уже нарушает " тот же поток будет производить и потреблять данные, хранящиеся в сумке".
Также я обнаружил, что хранилище ThreadLocal, созданное в каждом потоке в параллеле, будет существовать после операции (даже если поток используется повторно, это правильно?), Что может вызвать нежелательную утечку памяти.
Я прямо в этом парне? Или достаточно простого или чистого метода удаления элементов в ConcurrentBag достаточно?

Ответ 1

Это похоже на использование ConcurrentBag. Локальные переменные потока являются членами пакета и будут иметь право на сбор мусора в то же время, когда сумка (очистка содержимого не освобождает их). Вы правы, что для вашего случая достаточно простого списка с блокировкой. Если работа, которую вы выполняете в цикле, имеет существенное значение, тип синхронизации потоков не будет иметь большого значения для общей производительности. В этом случае вам может быть удобнее использовать то, с чем вы знакомы.

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

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

Ответ 2

Мне кажется, что bmm6o неверен. Экземпляр ConcurrentBag внутренне содержит мини-пакеты для каждого потока, который добавляет к нему элементы, поэтому вставка элемента не включает никаких блокировок потоков, и, таким образом, все потоки Environment.ProcessorCount могут полностью размахивать, не застревая в ожидании и без какого-либо контекста потока переключатели. Синхронизация нити может потребоваться при повторении по собранным элементам, но опять же в исходном примере итерация выполняется одним потоком после завершения всех вставок. Более того, если ConcurrentBag использует методы блокировки как первый уровень синхронизации потоков, то возможно вообще не включать операции мониторинга.

С другой стороны, использование обычного экземпляра List<T> и перенос каждого вызова метода Add() с ключевым словом блокировки сильно повредит производительность. Во-первых, из-за постоянных вызовов Monitor.Enter() и Monitor.Exit(), для каждого из которых требуется шаг за шагом в режиме ядра и для работы с примитивами синхронизации Windows. Во-вторых, иногда иногда один поток может быть заблокирован вторым потоком, потому что второй поток еще не завершил его добавление.

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