Friday, July 25, 2008

Timers are a Changin' (Part 2)

Today, I take a look at System.Timers.Timer. I suggested an article in my last discussion of timers. The article was written back in 2004, and it says that both the System.Windows.Forms.Timer and the System.Timers.Timer are found in the IDE Toolbox, one on the Windows Forms tab and the other on the Components tab. Well in my stock installation of Visual Studio 2008, both of those timers are System.Windows.Forms.Timer components. This may be due to some install problem I didn't know I had, or it may be the article is correct for Visual Studio .Net 2003 but not 2008.

That's okay because you can just add System.Timers.Timer to your project the old fashioned way. I simply start a new Windows Forms project and name it TimerExample2. After dropping a label on my form, I add the following code to my class file (I include a full listing at the end).


System.Timers.Timer timer1; //added
public Form1()
{
InitializeComponent();
// added the following
timer1 = new System.Timers.Timer(1000);
timer1.SynchronizingObject = this;
timer1.Elapsed += new System.Timers.ElapsedEventHandler(timer1_Elapsed);
timer1.Start();
}
void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
label1.Text = DateTime.Now.ToString("hh:mm:ss");
Console.WriteLine(e.SignalTime.ToString());
}


When I compile and run this application, I have a simple clock ticking away the seconds. Behaviorally speaking, its not much different from the last timer project. By keeping things simple, I can take a closer look at the differences. Besides the different constructor and handler, the thing that most stands out is the "SynchronizingObject" property. This property clues us in to one of the most fundamental differences between the standard Timer and the System.Timers.Timer. The System.Timers.Timer will actually run on its own dedicated thread. If I ran without setting the SynchronizingObject to "this" (my Form1), the timer would not have direct to my main GUI label1. Try it yourself, comment out the line that sets the SynchronizingObject and try running the application. It will throw an exception when it reaches the line that sets label1.Text. The SynchronizingObject in effect determines the thread on which the event handler will run, the timer still runs in a separate thread, but its event handler is run on the thread of the form you choose (in my case I chose Form1). Or, as I said, if you don't set the SynchronizingObject, the timer's event runs on its own thread and any interaction with the GUI will require Invoke type calls.

So let's test what I just said with some code. Let's go ahead and add some buttons and code like we did in our last discussion. I have a Button1 to launch a modal dialog box, Button2 to launch a modal form (Form2), and Button3 to "sleep" for 5 seconds. The modal operations stop the main event loop and start a sub-event loop, while the "Sleep" pauses the thread and all its event loops. I've wired these all up using the IDE, and I created a Form2 to be called. The handlers are below.


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);
}


When you run the application and click the first button, you will notice the timer continues to run even though we have a modal dialog. But, that's no surprise because that's what the last Timer we looked at did. We determined last time that the modal dialog's event loop was picking up the events and dispatching the parent's timer events though the other events were being eaten while the modal dialog was open. Similarly, clicking on the second button shows a form with the ShowDialog() call, a modal call. The timer still ticks away for the same reasons. And, when you click the third button, the clock stops ticking for 5 seconds. It doesn't appear to be any different until you take a moment to review the console output. When the Sleep returns, there are actually 5 timer events processed immediately following. Herein lies the difference. During the sleep the System.Timers.Timer continued to operate and post tick events. The last timer we looked at, System.Windows.Forms.Timer, does not post these events, its ticks are lost. By operating in its own thread, System.Timers.Timer is not effected by the GUI thread's call to Sleep().

To further demonstrate the threaded-ness of this Timer, let's look as some more code. I add another label (label2) and another Timer (timer2) to Form1. Then I add a handler for timer2 and a Button4 which instantiates and starts the timer, and some additional information to the Console.WriteLine to distinguish the two event handlers' output. The added code basically looks like this...


System.Timers.Timer timer2;
void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
string s = DateTime.Now.ToString("hh:mm:ss");
label2.Invoke(new MethodInvoker(() => label2.Text = s ));
Console.WriteLine("Timer2 {0}",e.SignalTime.ToString());
}

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();
}
}


If you run the application now, when you click Button4, the second timer starts ticking away, and when you click it again, it stops. When you click Button3, the main GUI sleeps for 5 seconds and both timers stop, but when the GUI awakes, notice that only timer1 continued to post events. What happened to timer2's events? Timer2 posts its events to the GUI through Invoke(). Invoke() is a blocking/synchronous call, so though timer2 is on its own thread, the thread was stopped waiting for Invoke to return which in turn was waiting for the GUI to service the invoke request, which couldn't happen while the GUI was asleep. So, I add Button5 and another event handler for timer2.


void timer2_ElapsedAsynch(object sender, System.Timers.ElapsedEventArgs e)
{
string s = DateTime.Now.ToString("hh:mm:ss");
label2.BeginInvoke(new MethodInvoker(() => label2.Text = s));
Console.WriteLine("Timer2 {0}", e.SignalTime.ToString());
}
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();
}
}


Now, when you click Button5 to start the timer2 and then click Button3 to put the GUI to sleep, both labels stop updating, but the console output shows that timer2 continues to fire events. When the sleep completes, the timer1 events appear in the output. This is still not quite the same behavior as using the SynchronizingObject which is actually post a message to the GUI to fire the event handler. Our little example fires the event handler on the Timer thread and posts an asynchronous message to Invoke the update on "Text". But we don't need the exact same behavior. In order to make the best use of a timer on a dedicated thread, its best to let the tick event handler process rather than blocking it through Invoke() or by synchronizing it through the GUI event loop. This way, should the GUI sleep (which it should not) or get bogged down with lots of events (which it very well could), the timer on its own thread will still get its event on time (roughly). And if the CPU isn't too consumed, the timer thread should get enough cycles to perform your custom timer operations.

Well, I really wanted to get this article out much sooner than this. But I still would like to see how the System.Timers.Timer works in conjunction with an event loop on a different thread than the main GUI. For example, put Form1 on one thread, Form2 on another thread and then play around with the SynchronizingObject variable and see what happens. But alas, I need to post this and move on to the last Timer in my next article. Maybe I'll come back to these, or you can post your findings.

Anyway, here's the code for the article. You need to ...
1) start a project called TimerExample2,
2) drop a couple of labels on Form1,
3) drop 5 buttons on Form1,
4) add another Form (Form2),
5) replace the code in your form1.cs with the code below, and
6) connect each of your buttons' click handlers to the appropriate handler below


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();
}

void timer1_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
label1.Text = DateTime.Now.ToString("hh:mm:ss");
Console.WriteLine("Timer1 {0}",e.SignalTime.ToString());
}
void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
string s = DateTime.Now.ToString("hh:mm:ss");
label2.Invoke(new MethodInvoker(() => label2.Text = s ));
Console.WriteLine("Timer2 {0}",e.SignalTime.ToString());
}
void timer2_ElapsedAsynch(object sender, System.Timers.ElapsedEventArgs e)
{
string s = DateTime.Now.ToString("hh:mm:ss");
label2.BeginInvoke(new MethodInvoker(() => label2.Text = s));
Console.WriteLine("Timer2 {0}", e.SignalTime.ToString());
}

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();
}
}
}
}

No comments: