Reputation: 397
This has actually bitten me a couple times. If you do simple code like this:
private void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i < 1000000; i++)
{
Thread CE = new Thread(SendCEcho);
CE.Priority = ThreadPriority.Normal;
CE.IsBackground = true;
CE.Start();
Thread.Sleep(500);
GC.Collect();
}
}
private void SendCEcho()
{
int Counter = 0;
for (int i = 0; i < 5; i++)
{
Counter++;
Thread.Sleep(25);
}
}
Run this code and watch the handles fly! Thread.Sleep is so you can shut it down and it doesn't take over you computer. This should guarantee that the thread launched dies before the next thread is launched. Calling GC.Collect(); does nothing. From my observation this code loses 10 handles every refresh to the task manager at normal refresh.
It doesn't matter what is in the void SendCEcho() function, count to five if you want. When the thread dies there is one handle that does not get cleaned up.
With most programs this doesn't really matter because they do not run for extended periods of time. On some of the programs I've created they need to run for months and months on end.
If you exercise this code over and over, you can eventually leak handles to the point where the windows OS becomes unstable and it will not function. Reboot required.
I did solve the problem by using a thread pool and code like this:
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadJobMoveStudy));
My question though is why is there such a leak in .Net, and why has it existed for so long? Since like 1.0? I could not find the answer here.
Upvotes: 7
Views: 3232
Reputation: 1073
Try adding GC.WaitForPendingFinalizers();
after your call to GC.Collect(); I think that will get you what you are after. Complete source below:
EDIT
Also take a look at this from 2005: https://bytes.com/topic/net/answers/106014-net-1-1-possibly-1-0-also-threads-leaking-event-handles-bug. Almost the exact same code as yours.
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 1000000; i++)
{
Thread CE = new Thread(SendCEcho);
CE.Priority = ThreadPriority.Normal;
CE.IsBackground = true;
CE.Start();
Thread.Sleep(500);
CE = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
public static void SendCEcho()
{
int Counter = 0;
for (int i = 0; i < 5; i++ )
{
Counter++;
Thread.Sleep(25);
}
}
}
Upvotes: 4
Reputation: 941635
for (int i = 0; i < 5 + i++; )
Fairly bizarre typo, you have not re-created your real problem. There is one, a Thread object consumes 5 operating system handles, its internal finalizer releases them. A .NET class normally has a Dispose() method to ensure that such handles can be released early but Thread does not have one. That was courageous design, such a Dispose() method would be very hard to call.
So having to rely on the finalizer is a hard requirement. In a program that has a "SendEcho" method, and does not do anything else, you are running the risk that the garbage collector never runs. So the finalizer can't do its job. It is then up to you to call GC.Collect() yourself. You'd consider doing so every, say, 1000 threads you start. Or use ThreadPool.QueueUserWorkItem() or Task.Run() so you recycle the threads, the logical approach.
Use Perfmon.exe to verify that the GC indeed doesn't run. Add the .NET CLR Memory > # Gen 0 Collections counter for your program.
Upvotes: 5
Reputation: 116411
You're creating threads that never end. The for loop in SendCEcho
never terminates, so the threads never end and thus cannot be reclaimed. If I fix your loop code then the threads finish and are reclaimed as expected. I'm unable to reproduce the problem with the code below.
static void SendCEcho()
{
int Counter = 0;
for (int i = 0; i < 5; i++)
{
Counter++;
Thread.Sleep(25);
}
}
Upvotes: 5