Używam BlockingCollection
do wdrożenia wzorca producent-konsument w C# 4.0.BlockingCollection w Task Parallel Library nie zwalnia automatycznie odwołania do podstawowych instancji
BlockingCollection
zawiera przedmioty, które zajmują sporo miejsca. Chciałbym pozwolić producentowi wziąć jeden przedmiot z BlockingCollection na raz i przetworzyć go.
Myślałem, że za pomocą foreach na BlockingCollection.GetConsumingEnumerable()
, za każdym razem, BlockingCollection
usunie element z podstawowej kolejki (co oznacza wszystko razem z referencją), tak na końcu metody Process(), która przetwarza przedmiot , przedmiot może być zbierany śmieci.
Ale to nie jest prawda. Wygląda na to, że pętla foreach na BlockingCollection.GetConsumingEnumerable()
zawiera wszystkie odwołania elementów wprowadzonych do kolejki. Wszystkie przedmioty są przechowywane (przez co nie mogą być zbierane śmieci) do momentu wyjścia z pętli foreach.
Zamiast używać prostej pętli foreach na BlockingCollection.GetConsumingEnumerable()
, używam pętli while testującej BlockingCollection.IsComplete, a wewnątrz pętli używam BlockingCollection.Take()
, aby pobrać materiał eksploatacyjny. Zakładam, że BlockingCollection.Take()
ma podobny efekt jak List.Remove()
, co spowoduje usunięcie odniesienia do elementu z BlockingCollection. Ale znowu to jest złe. Wszystkie przedmioty są tylko śmieciami zebranymi poza pętlą while.
Moje pytanie brzmi: w jaki sposób możemy łatwo wdrożyć wymóg, aby BlockingCollection potencjalnie zawierał artykuły zużywające dużo pamięci i aby każdy element mógł zostać zebrany podczas konsumpcji przez konsumenta? Dziękuję bardzo za pomoc.
EDIT: zgodnie z wnioskiem, prosty kod demo dodaje:
// Entity is what we are going to process.
// The finalizer will tell us when Entity is going to be garbage collected.
class Entity
{
private static int counter_;
private int id_;
public int ID { get{ return id_; } }
public Entity() { id_ = counter++; }
~Entity() { Console.WriteLine("Destroying entity {0}.", id_); }
}
...
private BlockingCollection<Entity> jobQueue_ = new BlockingCollection<Entity>();
private List<Task> tasks_ = new List<Task>();
// This is the method to launch and wait for the tasks to finish the work.
void Run()
{
tasks_.Add(Task.Factory.StartNew(ProduceEntity);
Console.WriteLine("Start processing.");
tasks_.Add(Task.Factory.StartNew(ConsumeEntity);
Task.WaitAll(tasks_.ToArray());
}
// The producer creates Entity instances and add them to BlockingCollection.
void ProduceEntity()
{
for(int i = 0; i < 10; i ++) // We are adding totally 10 entities.
{
var newEntity = new Entity();
Console.WriteLine("Create entity {0}.", newEntity.ID);
jobQueue_.Add(newEntity);
}
jobQueue_.CompleteAdding();
}
// The consumer takes entity, process it (and what I need: destroy it).
void ConsumeEntity()
{
while(!jobQueue_.IsCompleted){
Entity entity;
if(jobQueue_.TryTake(entity))
{
Console.WriteLine("Process entity {0}.", entity.ID);
entity = null;
// I would assume after GC, the entity will be finalized and garbage collected, but NOT.
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
Console.WriteLine("Finish processing.");
}
Wyjście jest to, że wszystkie komunikaty tworzenia i przetwarzania, a następnie „Zakończ przetwarzanie.” a następnie wszystkie wiadomości o zniszczeniu od podmiotów. I podmioty tworzenia wiadomości pokazując Entity.ID od 0 do 9, a komunikaty pokazujące zniszczenia Entity.ID od 9 do 0.
EDIT:
Nawet kiedy ustawić związanego zdolności BlockingCollection, wszystkie elementy kiedykolwiek wprowadzając go kończą się dopiero po wyjściu pętli, co jest dziwne.
Tylko dlatego, że nie jest ref przetrzymywany nie oznacza GC będzie wkroczyć od razu i zebrać je ... przykładowy kod, który pokazuje problem z odpowiednimi GC.Collect itp metod byłoby bądź pomocny –