Wednesday, July 30, 2008

Timers are a Changin' (Part 3)

In my last couple of articles, we saw the System.Windows.Forms.Timer and the System.Timers.Timer. Today, I shall look at the 3rd form of Timer in the .NET library, the System.Threading.Timer. I refer you again to the that very good article I mentioned a while back, Comparing the Timer Classes in the .NET Framework Class Library by Alex Calvo. Take a moment to read his section on System.Threading.Timer then come back and we will jump right into some code...


using System.Threading;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
timer1 = new System.Threading.Timer(new TimerCallback(timer_elapsed), null, 2000, 1000);
}

System.Threading.Timer timer1;
private void timer_elapsed(object sender)
{
Console.WriteLine(DateTime.Now.ToString());
TimerEvent(DateTime.Now.ToString(), Thread.CurrentThread.GetHashCode().ToString());
}

private delegate void TimerEventDelegate(string label, string name);
private void TimerEvent(string label, string name)
{
if (label1.InvokeRequired)
{
BeginInvoke(new TimerEventDelegate(TimerEvent), new object[] { label, name });
}
else
{
label1.Text = "Thread:" + name + " - Time:" + label;
}
}
}



To use this code above, create a new project named TimerExample3 and drop a label on the form. Then copy the code into your form1.cs replacing everything inside of the Namespace. It's ready to run!

The extra features gained with this timer are the ability to tell it to start at a future time, and you can pass it a state object. But there is another feature that is not so obvious. That is, the timer actually runs on the thread pool. If the timer_elapsed handler were to get hung in a sleep or something, that's okay because the next timer event will occur on time and fire the timer_elapsed again on a different thread. But, watch out! If you do not program for re-entrancy, you'll likely have problems.

At the moment, I have no guards for re-entrancy because the Console.WriteLine and the BeginInvoke should finish well within 1 second interval. So then, let's look at the behavior. First, notice that it takes 2 seconds before anything happens. That's in accordance with the 2000 millisecond startup time I gave the timer. Then you see the label updated with the thread number and time once every second along with console output. This is roughly the same as the last projects, only it took some very different code to get there.

Now, lets add a modal dialog to tie up the GUI thread and a button to tell the GUI to sleep as we did before. Here are the handlers, just drop two buttons on the form and wire up the handlers.


private void button1_Click(object sender, EventArgs e)
{
FolderBrowserDialog f = new FolderBrowserDialog();
f.ShowDialog();
}
private void button2_Click(object sender, EventArgs e)
{
Thread.Sleep(5000);
}


Run the application and experiment with the buttons. The console output does not miss a beat regardless of which button you click. The form continues to update the label with the time while the modal dialog is visible, but not when clicking the button to sleep. So far, so good. No real difference from our last timer.

Next, modify the handler by adding a Sleep() call after the TimerEvent() call. This will simulate some long running timer event processing for the sake of making the event handler exceed the programmed interval. Then run and look at the thread number and time.


private void timer_elapsed(object sender)
{
Console.WriteLine(DateTime.Now.ToString());
TimerEvent(DateTime.Now.ToString(), Thread.CurrentThread.GetHashCode().ToString());
Thread.Sleep(2000);
}


Sleep(2000) ties up the thread running timer_elapsed() callback. So, the Timer simply allocates another thread to handle the next timer interval. You can see this in the thread number in the form label, there are different thread numbers for each update, probably alternating between two values as my version does. If you recall then, the System.Windows.Forms.Timer runs in the same thread as the form. The System.Timers.Timer runs in a thread different than the GUI and has to be synchronized to the GUI of choice. Now we see that the System.Threading.Timer runs in a separate thread allocated from the thread pool.

This got me to thinking. Is the System.Timers.Timer also allocated from the thread pool? With a few quick modifications to my TimerExample2 application, I found the answer is yes!

I thought the System.Timers.Timer it would be on its own dedicated thread. Finding that it's not, we need to realize that we must deal with re-entrancy in both System.Timers.Timer and System.Threading.Timer. This was not mentioned or wasn't clear in the article I referenced. The good news is that you can treat re-entrancy the same way for both handlers.

So, the only real new features of the System.Threading.Timer are the delayed start and being able to pass in a state variable. So I will be revisiting my last article to correct any dis-information I may have provided about single-threadedness. But, now I wonder, will the System.Timers.Timer "elapsed" handler provides useful information in the "object sender" parameter? Does this serve as nearly a state variable? Maybe, I will post again soon on these and other questions I find.

For now, here's the code for TimerExample3. I figure I should just let you have what we've got until more research has been done.


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace TimerExample3
{
using System.Threading;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
timer1 = new System.Threading.Timer(new TimerCallback(timer_elapsed), null, 5000, 1000);
}

System.Threading.Timer timer1;
private void timer_elapsed(object sender)
{
Console.WriteLine(DateTime.Now.ToString());
TimerEvent(DateTime.Now.ToString(), Thread.CurrentThread.GetHashCode().ToString());
}

private delegate void TimerEventDelegate(string label, string name);
private void TimerEvent(string label, string name)
{
if (label1.InvokeRequired)
{
BeginInvoke(new TimerEventDelegate(TimerEvent), new object[] { label, name });
}
else
{
label1.Text = "Thread:" + name + " - Time:" + label;
}
}
private void button1_Click(object sender, EventArgs e)
{
FolderBrowserDialog f = new FolderBrowserDialog();
f.ShowDialog();
}
private void button2_Click(object sender, EventArgs e)
{
Thread.Sleep(5000);
}
}
}



... and here is my updated version of TimerExamples2, notice that I added a Button6 ...


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Threading;

namespace TimerExample2
{
public partial class Form1 : Form
{
System.Timers.Timer timer1;
System.Timers.Timer timer2;
public Form1()
{
InitializeComponent();
timer1 = new System.Timers.Timer(1000);
timer1.SynchronizingObject = this;
timer1.Elapsed += new System.Timers.ElapsedEventHandler(timer1_Elapsed);
timer1.Start();
}

private string theText(string signal)
{
string name = "Thread: " + Thread.CurrentThread.GetHashCode().ToString();
string stime = "Time: " + DateTime.Now.ToString("hh:mm:ss");
string s = name + " - " + stime + " - SignalTime: " + signal;
return s;
}
void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{

Console.WriteLine("Timer1 {0}",theText(e.SignalTime.ToString()));
label1.Text = theText(e.SignalTime.ToString());
}
void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
string s = theText(e.SignalTime.ToString());
Console.WriteLine("Timer2 {0}",s);
label2.Invoke(new MethodInvoker(() => label2.Text = s));
}
void timer2_ElapsedAsynch(object sender, System.Timers.ElapsedEventArgs e)
{
string s = theText(e.SignalTime.ToString());
Console.WriteLine("Timer2 {0}", s);
label2.BeginInvoke(new MethodInvoker(() => label2.Text = s));
}
void timer2_ElapsedAsynchSleep(object sender, System.Timers.ElapsedEventArgs e)
{
string s = theText(e.SignalTime.ToString());
Console.WriteLine("Timer2 {0}", s);
label2.BeginInvoke(new MethodInvoker(() => label2.Text = s));
Thread.Sleep(2000);
}

private void button1_Click(object sender, EventArgs e)
{
FolderBrowserDialog fb = new FolderBrowserDialog();
fb.ShowDialog();
}

private void button2_Click(object sender, EventArgs e)
{
Form2 frm = new Form2();
frm.ShowDialog();
}

private void button3_Click(object sender, EventArgs e)
{
Thread.Sleep(5000);
}

private void button4_Click(object sender, EventArgs e)
{
if (timer2 != null)
{
timer2.Stop();
timer2.Dispose();
timer2 = null;
}
else
{
timer2 = new System.Timers.Timer(1000);
timer2.Elapsed += new System.Timers.ElapsedEventHandler(timer2_Elapsed);
timer2.Start();
}
}

private void button5_Click(object sender, EventArgs e)
{
if (timer2 != null)
{
timer2.Stop();
timer2.Dispose();
timer2 = null;
}
else
{
timer2 = new System.Timers.Timer(1000);
timer2.Elapsed += new System.Timers.ElapsedEventHandler(timer2_ElapsedAsynch);
timer2.Start();
}
}

private void button6_Click(object sender, EventArgs e)
{
if (timer2 != null)
{
timer2.Stop();
timer2.Dispose();
timer2 = null;
}
else
{
timer2 = new System.Timers.Timer(1000);
timer2.Elapsed += new System.Timers.ElapsedEventHandler(timer2_ElapsedAsynchSleep);
timer2.Start();
}
}
}
}

No comments: