Есть ли утечка памяти в реализации ConcurrentBag <T>?
Возможный дубликат:
Возможная память в ConcurrentBag?
Edit1:
Реальный вопрос. Можете ли вы подтвердить это или мой образец неправильно, и я не вижу ничего очевидного?
Я думал, что ConcurrentBag просто заменяет список unorderd. Но я был неправ. ConcurrentBag добавляет себя как ThreadLocal к создающему потоку, который в основном вызывает утечку памяти.
class Program
{
static void Main(string[] args)
{
var start = GC.GetTotalMemory(true);
new Program().Start(args);
Console.WriteLine("Diff: {0:N0} bytes", GC.GetTotalMemory(true) - start);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Thread.Sleep(5000);
}
private void Start(string[] args)
{
for (int i = 0; i < 1000; i++)
{
var bag = new ConcurrentBag<byte>();
bag.Add(1);
byte by;
while (bag.TryTake(out by)) ;
}
}
Я могу сделать Diff 250 КБ или 100 ГБ в зависимости от того, сколько данных я добавляю в пакеты. Данные и мешки уходят.
Когда я врываюсь в это с помощью Windbg, и я делаю
Параметр DumpHeap Concurrent
....
000007ff00046858 1 24 System.Threading.ThreadLocal`1+GenericHolder`3[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System],[System.Threading.ThreadLocal`1+C0[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System]], mscorlib],[System.Threading.ThreadLocal`1+C0[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System]], mscorlib],[System.Threading.ThreadLocal`1+C0[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System]], mscorlib]]
000007feed812648 2 64 System.Collections.Concurrent.ConcurrentStack`1[[System.Int32, mscorlib]]
000007feece41528 1 112 System.Collections.Concurrent.CDSCollectionETWBCLProvider
000007ff000469e0 1000 32000 System.Threading.ThreadLocal`1+Boxed[[System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]], System]]
000007feed815900 1000 32000 System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Int32, mscorlib]]
000007ff00045530 1000 72000 System.Collections.Concurrent.ConcurrentBag`1+ThreadLocalList[[System.Byte, mscorlib]]
Когда я создаю пустой ConcurrentBag, чтобы некоторые рабочие потоки добавляли в него данные, ConcurrentBag и его данные будут там, пока создающий поток все еще жив.
Таким образом, я получил утечку памяти в несколько ГБ. Я "исправил" это, используя список и блокировки. ConcurrentBag может быть быстрым, но бесполезно, как простая замена списка с тем же самым сроком жизни объекта.
Если я когда-либо создаю ConcurrentBag в основном потоке, я буду хранить его до тех пор, пока поток будет жив. Это не то, что я ожидаю, и это может вызвать серьезную боль.
Ответы
Ответ 1
Вы правы, что ConcurrentBag создает копию ThreadLocal, на самом деле они оптимизированы для сценариев, где один и тот же поток читает и записывает данные в сумку: "... ConcurrentBag - это реалистичная реализация пакетов, оптимизированная для сценариев где тот же поток будет производить и потреблять данные, хранящиеся в сумке".
С другой стороны, я не вижу здесь странного поведения; нить живет, и совместная сумка живет. Когда поток заканчивается, GC выполнит эту работу.
Ответ 2
Из документации
ConcurrentBag представляет собой реалистичную реализацию пакетов, оптимизированную для сценариев, где один и тот же поток будет производить и потреблять данные, хранящиеся в сумке.
и Когда использовать поточно-безопасную коллекцию
В смешанных сценариях производитель-потребитель ConcurrentBag обычно намного быстрее и более масштабируемо, чем любой другой тип одновременного сбора для больших и малых рабочих нагрузок.
Я бы сказал, что ваши предположения о ConcurrentBag неверны. Во-первых, он не добавляет его в ThreadLocal, он использует локальное хранилище потоков для предоставления отдельных внутренних списков для каждого потока, который обращается к нему. Это больше, чем просто беспорядочный неупорядоченный список.
То, что вы считаете утечкой памяти, на самом деле является ожидаемым поведением, когда вы понимаете, что сумка использует TLS - нет необходимости очищать данные до тех пор, пока поток используется.
Сказав все это, я до сих пор не понял дополнительных функций ConcurrentBag, пока только.
Я нашел очень хорошее описание того, как ConcurrentBag использует отдельные списки и стоимость его методов в разных сценариях в Что такое ConcurrentBag ". Я хотел бы, чтобы это описание появилось в документации MSDN.
Лично я начну использовать ConcurrentBag намного больше, чтобы узнать о его особом поведении.
UPDATE:
Просто проверил этот пост Айенде, говоря, что "ThreadLocal, который использует ConcurrentBag, не ожидал, что будет много экземпляров. Это исправлено, и теперь может работать довольно быстро"
Ответ 3
Почему бы не переместить Console.WriteLine после второго GC.Collect()? В противном случае вы можете искать еще несколько объектов, чем вы планировали.
Вы также можете попробовать положить все в свой Main в цикл, чтобы получить несколько статистических данных. Даже если вы не будете перемещать свою запись, вы, вероятно, позже увидите более мелкие дельта.
Ура!