System.Timers.Timer가 GC에서 살아남지 만 System.Threading.Timer가 아닌 이유는 무엇입니까?
것으로 보인다 System.Timers.Timer
인스턴스가 어떤 메커니즘에 의해 살아 유지되지만 System.Threading.Timer
인스턴스는 없습니다.
주기적 System.Threading.Timer
및 자동 재설정 기능 이있는 샘플 프로그램 System.Timers.Timer
:
class Program
{
static void Main(string[] args)
{
var timer1 = new System.Threading.Timer(
_ => Console.WriteLine("Stayin alive (1)..."),
null,
0,
400);
var timer2 = new System.Timers.Timer
{
Interval = 400,
AutoReset = true
};
timer2.Elapsed += (_, __) => Console.WriteLine("Stayin alive (2)...");
timer2.Enabled = true;
System.Threading.Thread.Sleep(2000);
Console.WriteLine("Invoking GC.Collect...");
GC.Collect();
Console.ReadKey();
}
}
이 프로그램 (.NET 4.0 Client, Release, 디버거 외부)을 System.Threading.Timer
실행할 때만 GC가 적용됩니다.
Stayin alive (1)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Invoking GC.Collect...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
Stayin alive (2)...
편집 : 아래 John의 답변을 수락했지만 조금 설명하고 싶었습니다.
위의 샘플 프로그램을 실행할 때 (중단 점에서 Sleep
), 문제의 객체와 GCHandle
테이블 의 상태는 다음과 같습니다.
!dso
OS Thread Id: 0x838 (2104)
ESP/REG Object Name
0012F03C 00c2bee4 System.Object[] (System.String[])
0012F040 00c2bfb0 System.Timers.Timer
0012F17C 00c2bee4 System.Object[] (System.String[])
0012F184 00c2c034 System.Threading.Timer
0012F3A8 00c2bf30 System.Threading.TimerCallback
0012F3AC 00c2c008 System.Timers.ElapsedEventHandler
0012F3BC 00c2bfb0 System.Timers.Timer
0012F3C0 00c2bfb0 System.Timers.Timer
0012F3C4 00c2bfb0 System.Timers.Timer
0012F3C8 00c2bf50 System.Threading.Timer
0012F3CC 00c2bfb0 System.Timers.Timer
0012F3D0 00c2bfb0 System.Timers.Timer
0012F3D4 00c2bf50 System.Threading.Timer
0012F3D8 00c2bee4 System.Object[] (System.String[])
0012F4C4 00c2bee4 System.Object[] (System.String[])
0012F66C 00c2bee4 System.Object[] (System.String[])
0012F6A0 00c2bee4 System.Object[] (System.String[])
!gcroot -nostacks 00c2bf50
!gcroot -nostacks 00c2c034
DOMAIN(0015DC38):HANDLE(Strong):9911c0:Root: 00c2c05c(System.Threading._TimerCallback)->
00c2bfe8(System.Threading.TimerCallback)->
00c2bfb0(System.Timers.Timer)->
00c2c034(System.Threading.Timer)
!gchandles
GC Handle Statistics:
Strong Handles: 22
Pinned Handles: 5
Async Pinned Handles: 0
Ref Count Handles: 0
Weak Long Handles: 0
Weak Short Handles: 0
Other Handles: 0
Statistics:
MT Count TotalSize Class Name
7aa132b4 1 12 System.Diagnostics.TraceListenerCollection
79b9f720 1 12 System.Object
79ba1c50 1 28 System.SharedStatics
79ba37a8 1 36 System.Security.PermissionSet
79baa940 2 40 System.Threading._TimerCallback
79b9ff20 1 84 System.ExecutionEngineException
79b9fed4 1 84 System.StackOverflowException
79b9fe88 1 84 System.OutOfMemoryException
79b9fd44 1 84 System.Exception
7aa131b0 2 96 System.Diagnostics.DefaultTraceListener
79ba1000 1 112 System.AppDomain
79ba0104 3 144 System.Threading.Thread
79b9ff6c 2 168 System.Threading.ThreadAbortException
79b56d60 9 17128 System.Object[]
Total 27 objects
As John pointed out in his answer, both timers register their callback (System.Threading._TimerCallback
) in the GCHandle
table. As Hans pointed out in his comment, the state
parameter is also kept alive when this is done.
As John pointed out, the reason System.Timers.Timer
is kept alive is because it is referenced by the callback (it is passed as the state
parameter to the inner System.Threading.Timer
); likewise, the reason our System.Threading.Timer
is GC'ed is because it is not referenced by its callback.
Adding an explicit reference to timer1
's callback (e.g., Console.WriteLine("Stayin alive (" + timer1.GetType().FullName + ")")
) is sufficient to prevent GC.
Using the single-parameter constructor on System.Threading.Timer
also works, because the timer will then reference itself as the state
parameter. The following code keeps both timers alive after the GC, since they are each referenced by their callback from the GCHandle
table:
class Program
{
static void Main(string[] args)
{
System.Threading.Timer timer1 = null;
timer1 = new System.Threading.Timer(_ => Console.WriteLine("Stayin alive (1)..."));
timer1.Change(0, 400);
var timer2 = new System.Timers.Timer
{
Interval = 400,
AutoReset = true
};
timer2.Elapsed += (_, __) => Console.WriteLine("Stayin alive (2)...");
timer2.Enabled = true;
System.Threading.Thread.Sleep(2000);
Console.WriteLine("Invoking GC.Collect...");
GC.Collect();
Console.ReadKey();
}
}
You can answer this and similar questions with windbg, sos, and !gcroot
0:008> !gcroot -nostacks 0000000002354160
DOMAIN(00000000002FE6A0):HANDLE(Strong):241320:Root:00000000023541a8(System.Thre
ading._TimerCallback)->
00000000023540c8(System.Threading.TimerCallback)->
0000000002354050(System.Timers.Timer)->
0000000002354160(System.Threading.Timer)
0:008>
In both cases, the native timer has to prevent GC of the callback object (via a GCHandle). The difference is that in the case of System.Timers.Timer
the callback references the System.Timers.Timer
object (which is implemented internally using a System.Threading.Timer
)
I have been googling this issue recently after looking at some example implementations of Task.Delay and doing some experiments.
It turns out that whether or not System.Threading.Timer is GCd depends on how you construct it!!!
If constructed with just a callback then the state object will be the timer itself and this will prevent it from being GC'd. This does not appear to be documented anywhere and yet without it it is extremely difficult to create fire and forget timers.
I found this from the code at http://www.dotnetframework.org/default.aspx/DotNET/DotNET/8@0/untmp/whidbey/REDBITS/ndp/clr/src/BCL/System/Threading/Timer@cs/1/Timer@cs
The comments in this code also indicate why it is always better to use the callback-only ctor if the callback references the timer object returned by new as otherwise there could be a race bug.
In timer1 you're giving it a callback. In timer2 to you're hooking up an event handler; this setups up a reference to your Program class which means the timer won't be GCed. Since you never use the value of timer1 again, (basically the same as if you removed the var timer1 = ) the compiler is smart enough to optimize away the variable. When you hit the GC call, nothing is referencing timer1 anymore so its' collected.
Add a Console.Writeline after your GC call to output one of the properties of timer1 and you'll notice it's not collected anymore.
FYI, as of .NET 4.6 (if not earlier), this appears to not be true anymore. Your test program, when run today, does not result in either timer being garbage collected.
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Invoking GC.Collect...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
Stayin alive (2)...
Stayin alive (1)...
As I look at the implementation of System.Threading.Timer, this seems to make sense as it appears that the current version of .NET uses linked list of active timer objects and that linked list is held by a member variable inside TimerQueue (which is a singleton object kept alive by a static member variable also in TimerQueue). As a result, all timer instances will be kept alive as long as they are active.
'Programing' 카테고리의 다른 글
API를 사용하여 Google Play 개발자로부터 통계 얻기 (0) | 2020.11.06 |
---|---|
Angular-UI 대 Angular-Strap (0) | 2020.11.06 |
PDF 내에서 Javascript 사용 (0) | 2020.11.06 |
Android Firebase DynamiteModule : 모듈 설명자를로드하지 못했습니다. (0) | 2020.11.06 |
Java 5+의 휘발성이 다른 스레드의 가시성을 보장하지 않는 이유는 무엇입니까? (0) | 2020.11.06 |