닷넷에서 "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 와 그 차이점