I am currently developing a small simulation utility, using the Task Parallel Library to improve the speed at which results are produced. The simulation itself is a long, cpu intensive job which is essentially made up of thousands of smaller jobs running a simulation with different variables.
However, the resources used by each task are not released until everything has completed, leading to memory leaks and out of memory exceptions if enough variables are used. Forcing a GC at the end of each task releases resources, but my understanding is that this needs to interrupt all threads to execute, and as such results in close to single thread performance!
How can I release resources during long operations like this?
By ‘resources’ in this context I’m referring to arrays of doubles… just a lot of them.
public List<AnalysisTask> Questions; //Each variable combination is added as a Q
//Create a task for each simulation
Task<SimulationResults>[] tasks = new Task<SimulationResults>[Questions.Count];
foreach(var q in Questions)
{
AnalysisTask temp = q
tasks[taskCount] = Task.Factory.StartNew((t) =>
{
var result = EvaluateRules(temp);
if(reults.Value > Leader[0].Value)
Leader[0] = result;
else
{
result.Dispose();
//This releases resources but interrupts threads
//GC.Collect(2, GCCollectionMode.Forced);
return null;
}
return result;
}
}
//Completion task
Task.Factory.ContinueWhenAll(tasks, (ant) =>
{
DoSomethingWithAnswer(Leader[0]);
}
Perhaps I’ve taken the wrong approach in setting up the tasks? I’ll be grateful for any advice or direction 🙂
Your current implementation has a couple issues. One is that when an exchange is made with
Leader[0], the previous leader’s reference is lost and it is never disposed. This could be the source of your memory leak. The second is that the comparison and assignment toLeader[0]are not done atomically. It is possible to have this sequence of events: thread 1 compares toLeader[0]and gets true with aresult.Valueof 1, thread 2 compares toLeader[0]and gets true with aresult.Valueof 2, thread 2 writes toLeader[0], thread 1 writes toLeader[0]. The result is thatLeader[0]has a value of 1 when the maximal value was 2.So if we properly dispose of references you might not need to force garbage collection. The code below fixes those issues by taking out a lock when modifying
Leaderand storing a reference to the previousLeader[0]. Then either the unused result or previous leader is disposed. PresumablyEvaluateRuleswill take some time so there shouldn’t be much lock contention.Also, do you need to be returning
resultfrom each task? You seem to only needLeader[0]for your continuation task. By returningresultyou are storing a reference that cannot be gc’d until the tasks themselves are gc’d.