我是否需要保护垃圾收集器中的Thread
个对象?包含线程运行的函数的对象怎么样?
考虑这个简单的服务器:
class Server{
readonly TcpClient client;
public Server(TcpClient client){this.client = client;}
public void Serve(){
var stream = client.GetStream();
var writer = new StreamWriter(stream);
var reader = new StreamReader(stream);
writer.AutoFlush = true;
writer.WriteLine("Hello");
while(true){
var input = reader.ReadLine();
if(input.Trim() == "Goodbye")
break;
writer.WriteLine(input.ToUpper());
}
client.Close();
}
}
static void Main(string[] args){
var listener = new TcpListener(IPAddress.Any, int.Parse(args[0]));
listener.Start();
while(true){
var client = listener.AcceptTcpClient();
var server = new Server(client);
var thread = new Thread(server.Serve);
thread.Start();
}
}
我应该将我的线程对象包装在某种静态集合中,以防止它们被垃圾收集器扫除吗?
据推测,如果Thread
对象本身保持活动状态,则Server
对象将存活,因为该线程持有对委托的引用,该委托持有对目标对象的引用。或者可能收集线程对象本身,但实际线程继续运行。现在Server
对象即可进行收集。如果它试图访问其字段会发生什么?
垃圾收集使我的头部旋转了一些时间。我很高兴我通常不必考虑它。
考虑到潜在的问题,我想相信垃圾收集器足够智能,当线程本身仍在执行时不能收集线程对象,但我找不到任何文档说明。 Reflector在这方面没有多大帮助,因为Thread
类的大部分内容都在MethodImplOptions.InternalCall
函数中实现。而且我宁愿不通过我过时的旧SSCLI副本来寻找答案(因为这是一个痛苦,因为它不是一个肯定的答案)。
答案 0 :(得分:6)
这很简单。真正的执行线程不是Thread对象。该程序在真正的Windows线程中执行,无论.NET垃圾收集器对您的Thread对象执行什么操作,它都会保持活动状态。所以对你来说是安全的;如果你只是希望程序继续运行,你不需要关心Thread对象。
另请注意,您的线程在运行时不会被收集,因为它们实际上属于应用程序“根”。 (根源 - 垃圾收集者知道什么是活着的。)
更多详细信息:托管的Thread对象可以通过Thread.CurrentThread
访问 - 这类似于全局静态变量,并且不会收集这些变量。正如我之前写的:任何已启动并且现在正在执行任何代码(甚至在.NET之外)的托管线程都不会丢失其Thread对象,因为它与“根”紧密相连。
答案 1 :(得分:2)
只要你的线程正在执行,它就不会被收集。
答案 2 :(得分:1)
thread constructor中的start参数(在您的情况下是server.Serve)是您已经知道的委托。
推测,如果是Thread对象 本身保持活着,然后是服务器 对象会活,因为线程 持有对代表的引用 它包含对目标的引用 对象
这就是Jon Skeet在深度中的C#对代表目标的生命所说的话
值得注意的是一位代表 实例将阻止其目标 垃圾收集,如果 委托实例本身不可能 集。
只要server
在范围内,就不会收集var thread
。但是,如果var thread
在调用thread.start之前超出范围(并且没有其他引用),那么可以收集它。
现在是个大问题。调用Thread.Start()并且在server.Serve
完成之前线程已超出范围,GC可以收集线程。
而不是四处挖掘,只是测试它
class Program
{
static void Main(string[] args)
{
test();
GC.Collect(2);
GC.WaitForPendingFinalizers();
Console.WriteLine("test is over");
}
static void test()
{
var thread = new Thread(() => {
long i = 0;
while (true)
{
i++;
Console.WriteLine("test {0} {1} {2} ", Thread.CurrentThread.Name, Thread.CurrentThread.ManagedThreadId i);
Thread.Sleep(1000); //this is a demo so its okay
}
});
thread.Name = "MyThread";
thread.Start();
}
}
这是输出。 (在添加threadID之后)
test MyThread 3 1
test is over
test MyThread 3 2
test MyThread 3 3
test MyThread 3 4
test MyThread 3 5
所以我调用一个创建线程并启动它的方法。该方法结束,因此var thread
超出范围。但即使我已经引发了GC并且线程变量超出了范围,线程也会继续运行。
因此,只要您在超出范围之前启动线程,一切都将按预期工作
<强>更新强> 澄清GC.Collect
强制所有世代立即进行垃圾收集。
任何可以收集的东西都是。这不是评论所暗示的“仅仅是意图的表达”。 (如果没有理由不提出反对的话)但是我添加了参数“2”以确保它是gen0到gen2。
然而,关于终结者的观点值得注意。所以我添加了GC.WaitForPendingFinalizers
...挂起当前线程,直到处理终结器队列的线程清空该队列。
这意味着任何需要处理的终结器确实已被处理。
示例的重点是,只要你启动线程,它就会一直运行直到它被中止或完成,并且GC不会因为var thread
超出范围而以某种方式中止线程。
暂且放弃 Al Kepp是正确的,确实一旦启动线程,System.Threading.Thread就会生根。您可以使用SOS扩展程序找到它。
e.g。
!do 0113bf40
Name: System.Threading.Thread
MethodTable: 79b9ffcc
EEClass: 798d8ed8
Size: 48(0x30) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
79b88a28 4000720 4 ....Contexts.Context 0 instance aaef947d00000000 m_Context
79b9b468 4000721 8 ....ExecutionContext 0 instance aaef947d00000000 m_ExecutionContext
79b9f9ac 4000722 c System.String 0 instance 0113bf00 m_Name
79b9fe80 4000723 10 System.Delegate 0 instance 0113bf84 m_Delegate
79ba63a4 4000724 14 ...ation.CultureInfo 0 instance aaef947d00000000 m_CurrentCulture
79ba63a4 4000725 18 ...ation.CultureInfo 0 instance aaef947d00000000 m_CurrentUICulture
79b9f5e8 4000726 1c System.Object 0 instance aaef947d00000000 m_ThreadStartArg
79b9aa2c 4000727 20 System.IntPtr 1 instance 001D9238 DONT_USE_InternalThread
79ba2978 4000728 24 System.Int32 1 instance 2 m_Priority
79ba2978 4000729 28 System.Int32 1 instance 3 m_ManagedThreadId
79b8b71c 400072a 18c ...LocalDataStoreMgr 0 shared static s_LocalDataStoreMgr
>> Domain:Value 0017fd80:NotInit <<
79b8e2d8 400072b c ...alDataStoreHolder 0 shared TLstatic s_LocalDataStore
这是名为MyThread的线程
!do -nofields 0113bf00
Name: System.String
MethodTable: 79b9f9ac
EEClass: 798d8bb0
Size: 30(0x1e) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: MyThread
为什么是,它是。
根源是什么?
!GCRoot 0113bf40
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 7708 OSTHread 1e1c
Scan Thread 4572 OSTHread 11dc
Scan Thread 9876 OSTHread 2694
ESP:101f7ec:Root: 0113bf40(System.Threading.Thread)
ESP:101f7f4:Root: 0113bf40(System.Threading.Thread)
DOMAIN(0017FD80):HANDLE(Strong):9b11cc:Root: 0113bf40(System.Threading.Thread)
正如Production Debugging for .NET Framework Applications所说它是根。
请注意!GCRoot在启动后运行。在它开始之前它没有HANDLE(强)。
如果输出包含HANDLE(强), 找到了一个强有力的参考。这个 意味着对象是root的 不能垃圾收集。其他 引用类型可以在 附录
有关托管线程如何映射到操作系统线程的更多信息(以及如何使用SOS扩展确认它),请参阅Yun Jin的this article。
答案 3 :(得分:1)
尽管我的caution to Conrad Frix关于使用多线程或GC进行测试代码验证的难度,但更不用说两者了,我自己也只是进行了一个简单的小测试。我认为结果令人满意地接受了这个问题。
我越是阅读这里发布的答案,我想的越多,它就越能归结为一个特定的问题:
Thread
对象的终结器在我的线程退出之前运行时会发生多大的破坏。
显然,GC本身不会(直接)中止实际的执行线程。只要Server
对象的Serve()
方法正在执行,它的this
指针就应该被保护为当前正在运行的方法可以访问的对象。
但是如果GC收集并最终确定 Thread
对象,会发生什么?由于线程对象应该代表执行线程,它的终结器会杀死执行线程,使它们达到平衡吗?或者更糟糕的是,它会破坏运行时用来管理线程的一些内部状态吗?
我真的不想撬开Rotor回答这些问题,因为它是钝的,而且因为我不想依赖SSCLI实现;不是为了这个。
但后来我开始怀疑自己是不是在考虑这种情况。我认为Thread
对象是控制执行线程,暗示后者会与前者生存并死亡。
但事实上,Thread
类只是执行线程的接口 - 一个方便的抽象。相反,它必须与执行线程一起生存和死亡,而不是相反。
从这个角度来看,我认为框架必须已经完成了保持抽象的工作,对吧?当然!我用了一百万次。这是Thread.CurrentThread
属性!
最简单的测试表明,创建的Thread
对象确实可以从框架内部访问:
Thread t;
t = new Thread(o=>Console.WriteLine(o == Thread.CurrentThread));
t.Start(t);
这个微小的测试证实,无论我是否保留对Thread
个对象的引用,框架都会这样做。在知道的情况下,其他一切都是给定的。 Server
对象存在,执行线程继续执行。
耶!我可以回去不用考虑垃圾收集器一段时间了!