닷넷에서 "Timer"를 검색하면 클래스가 세 개 나옵니다. System.Windows.Forms.Timer, System.Threading.Timer, System.Timers.Timer. 이름이 같다 보니 대충 아무거나 가져다 쓰는 경우가 많은데, 셋은 이벤트가 실행되는 스레드가 다릅니다. 그 차이를 모르고 쓰면 "타이머 돌리는 동안 폼이 멈춘다"거나 "타이머에서 라벨을 바꾸니 크로스 스레드 예외가 난다" 같은 문제를 만나게 되죠. VB.NET에서도 클래스는 똑같으니 언어와 상관없이 알아 둘 내용입니다.
셋을 가르는 기준은 이벤트가 도는 스레드
| 항목 | System.Windows.Forms | System.Timers | System.Threading |
|---|---|---|---|
| 이벤트가 실행되는 스레드 | UI 스레드 | UI 또는 작업자 스레드 | 작업자 스레드 |
| 인스턴스가 스레드 안전한가 | No | Yes | No |
| 익숙하고 직관적인 객체 모델 | Yes | Yes | No |
| Windows Forms 필요 | Yes | No | No |
| 일정한 박자(메트로놈 수준) | No | Yes* | Yes* |
| 이벤트에 상태 객체 전달 | No | No | Yes |
| 첫 이벤트 시점 예약 가능 | No | No | Yes |
| 상속 가능 | Yes | Yes | No |
* 작업자 스레드 같은 시스템 자원 여유에 따라 달라집니다.
위 표는 MSDN Magazine에서 세 타이머를 비교하며 소개했던 내용입니다. 써 보면 가장 크게 와닿는 차이는 첫 줄, 이벤트가 어느 스레드에서 실행되느냐입니다. 나머지 차이는 대부분 여기서 파생됩니다.
Forms.Timer: UI 스레드에서 돌기 때문에 편하지만 막히기도 한다
폼 디자이너 도구상자에서 끌어다 놓는 그 타이머입니다. 이 타이머의 Tick 이벤트는 폼을 만든 UI 스레드, 즉 메시지 루프가 도는 스레드에서 실행됩니다. 그래서 Tick 안에서 라벨이나 텍스트박스를 바로 고쳐도 아무 문제가 없습니다.
System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
timer.Interval = 1000; //주기 설정(ms)
timer.Tick += new EventHandler(timer_Tick); //주기마다 실행되는 이벤트 등록
void tmrWindowsFormsTimer_Tick(object sender, System.EventArgs e)
{
//수행해야할 작업
}
시작과 중지는 다음처럼 합니다.
// 시작
timer.Enabled = true; // 또는 timer.Start();
// 중지
timer.Enabled = false; // 또는 timer.Stop();
주의: 위 코드는 원문 그대로 옮긴 것인데 그대로는 컴파일되지 않습니다. 속성 이름은 Enable이 아니라 Enabled이고, +=로 등록한 timer_Tick과 실제 정의한 메서드 이름(tmrWindowsFormsTimer_Tick)이 다릅니다. 실제로 쓸 때는 둘 중 하나로 이름을 맞추세요.
편한 대신 대가가 있습니다. 같은 스레드라는 건 곧 멀티스레드가 아니라는 뜻이라서, Tick 안에서 DB 조회나 파일 복사처럼 오래 걸리는 작업을 하면 그동안 폼 전체가 응답하지 않습니다. 타이머가 백그라운드에서 따로 돈다고 생각하면 안 됩니다. 또 메시지 루프가 있어야 이벤트가 오기 때문에 콘솔 프로그램이나 윈도우 서비스에서는 쓸 수 없습니다.
실제로 같은 스레드인지 확인해 보려면 스레드 번호를 찍어 보면 됩니다. 스레드마다 고유 번호는 Thread.CurrentThread.ManagedThreadId로 얻습니다. 폼 생성자나 Load 이벤트에서 한 번, Tick에서 한 번 출력해 비교하면 두 값이 같게 나옵니다.
// 폼 생성자나 Load 이벤트 등에서
MessageBox.Show(System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
void timer1_Tick(object sender, EventArgs e)
{
MessageBox.Show(System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
//수행해야할 작업
}
Threading.Timer: 스레드 풀에서 도는 가벼운 타이머
이벤트가 아니라 콜백 메서드를 생성자에 넘기는 방식입니다. 콜백은 닷넷 스레드 풀의 작업자 스레드에서 실행됩니다. 별도의 Start()가 없고, Change(dueTime, period)로 첫 실행까지의 대기 시간과 반복 주기를 지정하는 순간 동작합니다. dueTime을 0으로 주면 바로 시작합니다.
System.Threading.Timer timer = new System.Threading.Timer(CallBack);
timer.Change(0, 1000); // dueTime(시작 전 지연), period(반복 주기)
멈출 때는 두 값을 모두 무한대로 바꿉니다. 사실상 "다음 실행을 영원히 미룬다"는 뜻이죠.
timer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
콜백이 UI 스레드가 아닌 곳에서 실행되니, 여기서 폼 컨트롤을 직접 건드리면 크로스 스레드 예외가 납니다. 컨트롤을 고칠 작업은 Invoke나 BeginInvoke로 UI 스레드에 넘겨야 합니다.
delegate void TimerEventFiredDelegate();
void CallBack(Object state)
{
BeginInvoke(new TimerEventFiredDelegate(Work));
}
private void Work()
{
//수행해야할 작업(UI Thread 핸들링 가능)
}
콜백에서 스레드 번호를 찍어 보면 폼의 스레드 번호와 다른 값이 나옵니다.
void CallBack(Object state)
{
MessageBox.Show(System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
BeginInvoke(new TimerEventFiredDelegate(Work));
}
UI와 독립적으로 돌기 때문에 콜백에서 시간이 걸리는 작업을 해도 폼이 굳지 않습니다. 대신 주기보다 작업이 오래 걸리면 다음 콜백이 다른 스레드에서 겹쳐 실행될 수 있다는 점은 염두에 둬야 합니다. 그리고 타이머 변수를 지역 변수로만 들고 있으면 가비지 컬렉션 대상이 되어 어느 순간 조용히 멈출 수 있으니, 필드로 잡아 두는 편이 안전합니다.
Timers.Timer: SynchronizingObject에 따라 달라진다
사용법은 Forms.Timer와 거의 같고, 이벤트 이름만 Elapsed입니다.
System.Timers.Timer timer = new System.Timers.Timer();
timer.Interval = 1000;
timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); //주기마다 실행되는 이벤트 등록
기본 상태에서는 Elapsed가 작업자 스레드에서 실행되므로, UI를 고치려면 역시 BeginInvoke를 거칩니다.
delegate void TimerEventFiredDelegate();
void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
BeginInvoke(new TimerEventFiredDelegate(Work));
}
private void Work()
{
//수행해야할 작업(UI Thread 핸들링 가능)
}
시작과 중지 방식도 Forms.Timer와 같습니다(여기서도 실제 속성명은 Enabled입니다).
// 시작
timer.Enabled = true; // 또는 timer.Start();
// 중지
timer.Enabled = false; // 또는 timer.Stop();
이 타이머만의 특징은 SynchronizingObject 속성입니다. 여기에 폼을 지정하면 Elapsed가 UI 스레드에서 실행되고, 지정하지 않으면 작업자 스레드에서 실행됩니다. 아래 한 줄을 넣고 빼면서 Elapsed 안에서 ManagedThreadId를 찍어 보면 차이가 바로 보입니다.
timer.SynchronizingObject = this;
MessageBox.Show(System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
그래서 뭘 쓰면 되나
폼에서 시계 표시나 상태 라벨 갱신처럼 가볍고 UI를 자주 만지는 일이면 Forms.Timer가 제일 덜 번거롭습니다. 주기적으로 무거운 작업을 돌려야 하는데 폼이 굳으면 안 되는 경우, 혹은 콘솔·서비스처럼 메시지 루프가 없는 환경이면 Timers.Timer나 Threading.Timer로 갑니다. 이벤트 방식이 익숙하고 나중에 UI 스레드로 붙일 가능성도 있다면 Timers.Timer, 상태 객체를 넘기거나 첫 실행 시점을 따로 정하고 싶다면 Threading.Timer가 맞습니다.
네임스페이스로 정리하면 Windows 기반 타이머는 System.Windows.Forms, 서버 기반 타이머는 System.Timers, 스레드 타이머는 System.Threading에 있습니다. 두 네임스페이스를 모두 using 해 둔 파일에서 Timer만 쓰면 모호한 참조(CS0104) 오류가 나니, 위 예제처럼 전체 이름으로 쓰는 습관을 들이면 헷갈릴 일이 줄어듭니다.
참고: 세가지 Timer 와 그 차이점