I’ve been debugging my program, because it throws Out Of Memory exceptions.
After stripping it down, it looks like a Task is holding onto a reference although it is done.
Here is my stripped down code:
public class Program {
static void Main(string[] args) {
var cts = new CancellationTokenSource();
ConsumeFiles(cts.Token);
}
public static void ConsumeFiles(CancellationToken ct) {
while(true) {
var dataSource = new Queue<byte[]>();
for(int i = 0;i < 16;i++) {
var block = new byte[4 * 1024 * 1024];
for(int j = 0;j < block.Length;j++) block[j] = (byte)j;
dataSource.Enqueue(block);
}
var cts = new CancellationTokenSource();
ct.Register(() => cts.Cancel()); //Remove this line => No OOM
Task.Factory.StartNew(() => { //Remove Task => No OOM
var b = dataSource; //Remove this line => No OOM
}).Wait();
}
}
}
In my unstripped code:
The CancellationTokenSource in Main is used to cancel the whole operation.
The one in ConsumeFiles is used to cancel a suboperation, in case something went wrong in some other suboperation in ConsumeFiles.
What do I need to do so that the old instance of dataSource is properly garbage collected?
And why doesn’t it work the way it is now?
It’s not the task holding onto a reference
This
is your only problem.
ctsis theCancellationTokenSourcethat was created in yourMainmethod. In each iteration of the loop, you are creating a newCancellationTokenSourceand then registering a callback where you callCancelon the new cts. When you register a callback, it’s passed into the token’s parent, which happens to be the token source inMain. Each time you register one, it will be added to the array, increasing it’s size as necessary.Because the token source in main is still in scope, it’s not eligible for garbage collection, and neither is the array of callbacks that it is holding onto internally.
Here are type stats from windbg:
1- After a minute or two:
2- A few minutes later:
3- A few more minutes:
Notice that each time I’ve run a stat on the heap, the number of allocated
byte[]changes (because they are getting GC’d), where the number ofCancellationTokenSource(the line where you new one up) types keeps increasing, along with theSystem.Action(the parameter you’ve passed toRegister) andCancellationCallbackInfo(the internal data structure created byRegister()to store the value).From the code you’ve given, it doesn’t make any sense to create a cancellation token source at all if you want to cancel multiple threads. You can pass the token parameter (
ct) into other tasks you want to cancel. Even though it’s a value type, and will be copied, the pointer to the token source remains, and you can still cancel them all with acts.Cancel()from Main. I’m not sure if that’s what you’re trying to do, though.