View | Details | Raw Unified | Return to bug 315999
Collapse All | Expand All

(-)class/corlib/System.Threading/Timer.cs (-121 / +268 lines)
Lines 4-9 Link Here
4
// Authors:
4
// Authors:
5
// 	Dick Porter (dick@ximian.com)
5
// 	Dick Porter (dick@ximian.com)
6
// 	Gonzalo Paniagua Javier (gonzalo@ximian.com)
6
// 	Gonzalo Paniagua Javier (gonzalo@ximian.com)
7
// 	Rafael Ferreira (raf@ophion.org)
7
//
8
//
8
// (C) 2001, 2002 Ximian, Inc.  http://www.ximian.com
9
// (C) 2001, 2002 Ximian, Inc.  http://www.ximian.com
9
// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
10
// Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
Lines 29-34 Link Here
29
//
30
//
30
31
31
using System.Runtime.InteropServices;
32
using System.Runtime.InteropServices;
33
using System.Collections;
34
using System;
32
35
33
namespace System.Threading
36
namespace System.Threading
34
{
37
{
Lines 37-160 Link Here
37
#endif
40
#endif
38
	public sealed class Timer : MarshalByRefObject, IDisposable
41
	public sealed class Timer : MarshalByRefObject, IDisposable
39
	{
42
	{
40
		sealed class Runner : MarshalByRefObject
43
		/*
41
		{
44
		
42
			ManualResetEvent wait;
45
		Timer Scheduler
43
			AutoResetEvent start_event;
46
		---------------
44
			TimerCallback callback;
47
		Author: Rafael Ferreira (raf@ophion.org)
45
			object state;
48
		
46
			int dueTime;
49
		The code below implements a single thread scheduler that fires
47
			int period;
50
		events using the runtime's built-in thread pool.
48
			bool disposed;
49
			bool aborted;
50
51
51
			public Runner (TimerCallback callback, object state, AutoResetEvent start_event)
52
		Key Features:
52
			{
53
			Single thread scheduler:
53
				this.callback = callback;
54
				A single thread handles firing all timer jobs thus allowing a 
54
				this.state = state;
55
				much greater number of Timers to be defined
55
				this.start_event = start_event;
56
			Lazy init:
56
				this.wait = new ManualResetEvent (false);
57
				Timer scheduler is only started after the first System.Threading.Timer is created
57
			}
58
			Early termination:
59
				Timer scheduler thread dies if there are no more timer jobs in its Job queue
60
				
61
			
62
		In a nutshell the scheduler works like this:
63
			1 - The main scheduler thread (TimerScheduler) wakes up finds out what time it is
64
			2 - The scheduler iterates over the list of timer jobs (Jobs) to find out when the next job is
65
			as well as fires all timers that were scheduled to run now or in the past
66
			3 - The Scheduler then calculates the mutiplier for its sleep algorithm. The multiplier
67
			is basically MSEC_UNTIL_NEXT_JOB / TIME_SLICE where TIME_SLICE is the minimum amount of 
68
			time the scheduler thread is allowed to sleep for
69
			4 - Sleep for multiplier * TIME_SLICE
70
			5 - Goto 1
58
71
59
			public int DueTime {
72
		Possible improvements:
60
				get { return dueTime; }
73
			* Convert the big for-loop into a sorted data structure. This will speed up the time
61
				set { dueTime = value; }
74
			it takes the scheduler to iterate over the timer jobs and lower CPU usage under insane 
62
			}
75
			amounts of Timers 
63
76
64
			public int Period {
77
		Possible issues:
65
				get { return period; }
78
			* Overflow issues with the multiplier
66
				set { period = value == 0 ? Timeout.Infinite : value; }
79
			* Race conditions with lazy-init of the scheduler thread.
67
			}
68
80
69
			bool WaitForDueTime ()
81
		Note:
70
			{
82
			MONO_DEBUG_TIMER environment variable can be used
71
				if (dueTime > 0) {
83
			to turn on the scheduler's debug log
72
					bool signaled;
84
		*/
73
					do {
74
						wait.Reset ();
75
						signaled = wait.WaitOne (dueTime, false);
76
					} while (signaled == true && !disposed && !aborted);
77
85
78
					if (!signaled)
86
		class TimerScheduler {
79
						callback (state);
87
			private class TimerJob {
80
88
				
81
					if (disposed)
89
				public long NextRun;
82
						return false;
90
				public long LastRun;
91
				public long Period;
92
				public bool Enabled;
93
				public TimerCallback Callback;
94
				public readonly int ID;
95
				public object State = null;
96
				public TimerJob(int id) {
97
					ID = id;
98
					Enabled = true;
83
				}
99
				}
84
				else
85
					callback (state);
86
87
				return true;
88
			}
100
			}
101
			// this enum is used to signal the scheduler thread of the reason for the Abort() call
102
			// abort() is used to signal the timer thread since Interrupt() is not implemented
103
			private enum AbortSignals { TIMER_ADDED, TIMER_REMOVED, TIMER_CHANGED };
104
			
105
			readonly int TIME_SLICE = 10 ; // 10 msec
106
			int next_id = 0;
89
107
90
			public void Abort ()
108
			Thread scheduler = null;
91
			{
109
			static object sync_obj = new object();
92
				lock (this) {
110
			static object sync_obj2 = new object();
93
					aborted = true;
111
			
94
					wait.Set ();
112
			Hashtable Jobs =  new Hashtable();
113
			bool scheduler_ready = false;
114
			
115
			static TimerScheduler _instance = null;
116
117
			protected TimerScheduler() {}
118
			
119
			// singleton magic
120
			public static TimerScheduler GetInstance() {
121
				if (_instance == null) {
122
					lock(sync_obj) {
123
						_instance = new TimerScheduler();
124
					}
95
				}
125
				}
126
				return(_instance);
96
			}
127
			}
97
			
128
98
			public void Dispose ()
129
			public int AddJob(TimerCallback cb, object state, int dueTime, int period) {
99
			{
130
				TimerJob tj = new TimerJob(Interlocked.Increment(ref next_id)); 
100
				lock (this) {
131
				
101
					disposed = true;
132
				// first run take into consideration the delay metric only
102
					Abort ();
133
				set_job_properties(tj,dueTime,period);
134
				tj.Callback = cb;
135
				tj.State = state;
136
				
137
				// lock job Q
138
				lock(Jobs) {
139
					Jobs.Add(tj.ID,tj);
103
				}
140
				}
141
				lock(sync_obj2) {
142
					send_scheduler_signal(AbortSignals.TIMER_ADDED);
143
				}
144
				
145
				return(tj.ID);
146
				
147
				
104
			}
148
			}
149
			// prorperly handles signaling the scheduler thread
150
			// it will retry for 500 msec (.5 sec) if it can't properly signal scheduler
151
			void send_scheduler_signal(AbortSignals signal) {
152
				for (int i = 0; i < 100; i++) {
105
153
106
			public void Start ()
154
					// we don't start a new scheduler if the signal is to  TIMER_REMOVED
107
			{
155
					if (scheduler == null) {
108
				while (!disposed && start_event.WaitOne ()) {
156
						log("Scheduler not currently running... new scheduler will be initiated");
109
					if (disposed)
157
						scheduler = new Thread( new ThreadStart(SchedulerThread));
158
						scheduler.IsBackground = true;
159
						scheduler.Start();
110
						return;
160
						return;
161
						
162
					} 
111
163
112
					aborted = false;
164
					if (scheduler.ThreadState == ThreadState.AbortRequested || 
165
					scheduler.ThreadState == ThreadState.Aborted) {
166
						return;
167
					}
113
168
114
					if (dueTime == Timeout.Infinite)
169
					if (scheduler_ready) {
115
						continue;
170
						// we batch send Abort() calls
116
171
						// Abort is used since Thread.Interrupt is not supported.
117
					if (!WaitForDueTime ())
172
						scheduler.Abort(signal);
118
						return;
173
						return;
174
						
175
					}
176
					log("could not properly signal timer-scheduler, waiting...");
177
					Thread.Sleep(5); 
178
				}
179
				
180
				throw new Exception("Could not properly abort timer-scheduler thread");
181
				
182
			
183
			}
184
			void set_job_properties(TimerJob tj,int dueTime, int period) {
185
				if (dueTime == Timeout.Infinite) {
186
					//disables the job
187
					log("disabling job " + tj.ID);
188
					tj.Enabled = false;
189
				}else {
190
					tj.NextRun = DateTime.Now.Ticks + TimeSpan.TicksPerMillisecond * dueTime;
191
					tj.Enabled = true;
192
				}
193
				if (period == Timeout.Infinite) {
194
					log("job " + tj.ID  + " will only run once");
195
					tj.Period = -1;
196
				}else {
197
					tj.Period = TimeSpan.TicksPerMillisecond * period;
198
				}
199
				log(String.Format("timer job configured, id {2} delay {0} msec period {1} msec", dueTime, period, tj.ID));
119
200
120
					if (aborted || (period == Timeout.Infinite))
201
			}
121
						continue;
202
			public bool RemoveJob(int id) {
203
				lock (Jobs) {
204
					if (!Jobs.Contains(id)) {
205
						return(false);
206
					}
207
					Jobs.Remove(id);
208
				}
209
				lock (sync_obj2) {
210
					send_scheduler_signal(AbortSignals.TIMER_REMOVED);
211
				}
212
				log(String.Format("Job {0} removed",id));
213
				return(true);
214
			}
122
215
123
					bool signaled = false;
216
			public bool ChangeJob(int id, int dueTime, int period) {
124
					while (true) {
217
			
125
						if (disposed)
218
				// modifying the job is actually quicker (lock wise) than doing a Remove / Add combo
126
							return;
219
				lock (Jobs) {
220
					if (!Jobs.Contains(id)) {
221
						return(false);
222
					}
223
					TimerJob tj = Jobs[id] as TimerJob;
224
					set_job_properties(tj,dueTime,period);
225
					log("job " + id +" changed");
226
				}
227
				lock (sync_obj2) {
228
					send_scheduler_signal(AbortSignals.TIMER_CHANGED);
229
				}
230
				
231
				return(true);
232
			}
233
			
234
			public void SchedulerThread() {
235
				Thread.CurrentThread.Name = "Timer-Scheduler";
236
				long tick = 0;
237
				long next_job = Int64.MaxValue;
238
				TimerJob tj = null;
239
				int multiplier = 1;
240
				
241
				// big scary for-loop that iterates over the jobs
242
				while(Jobs.Count > 0) {
243
					if (!scheduler_ready)  {
244
						scheduler_ready=true;
245
						log("Scheduler is ready");
246
					}
127
247
128
						if (aborted)
248
					try {
129
							break;
249
						tick = DateTime.Now.Ticks;
250
						lock (Jobs) {
251
							foreach (DictionaryEntry entry in Jobs) {
252
								tj = entry.Value as TimerJob;
130
253
131
						try {
254
								if (tj.Enabled == false) {
132
							wait.Reset ();
255
									continue;
133
						} catch (ObjectDisposedException) {
256
								}
134
							// FIXME: There is some race condition
257
								if ( tj.NextRun <= tick) {
135
							//        here when the thread is being
258
									
136
							//        aborted on exit.
259
									// Firing job using runtime's thread pool
137
							return;
260
									log("Firing job " + tj.ID);
138
						}
261
									ThreadPool.QueueUserWorkItem(new WaitCallback(tj.Callback),tj.State);
262
									
263
									if (tj.Period == - 1) {
264
										// it is a run-once job, so we disable it
265
										tj.Enabled = false;
266
									}
267
									else {
268
										tj.NextRun = tick + tj.Period;
269
									}
270
									
271
									tj.LastRun = tick;
139
272
140
						signaled = wait.WaitOne (period, false);
273
									// we reset the next_job to the max possible value so the real next job
141
274
									// can be figured out
142
						if (aborted || disposed)
275
									next_job = Int64.MaxValue;
143
							break;
276
								}
144
277
								if ( next_job > tj.NextRun) {
145
						if (!signaled) {
278
									next_job = tj.NextRun;
146
							callback (state);
279
								}
147
						} else if (!WaitForDueTime ()) {
280
							}
148
							return;
149
						}
281
						}
282
						
283
						// no other jobs are available and all jobs
284
						// are disabled
285
						if (next_job == Int64.MaxValue) {
286
							log("no active jobs found, going into infinite sleep");
287
							Thread.Sleep(Timeout.Infinite);
288
						}else {
289
							
290
							multiplier = (int) ((next_job - tick) / TimeSpan.TicksPerMillisecond);
291
							multiplier = multiplier / TIME_SLICE;
292
							if (multiplier > 0 ) {
293
								//TODO there are some edgy race conditions between the abort signal and telling a thread 
294
								// to sleep
295
								log("gong to sleep for " + multiplier + " times the time slice");
296
								Thread.Sleep(multiplier * TIME_SLICE);
297
							}
298
						}
299
						
300
					} catch (ThreadAbortException ex) {
301
						if (ex.ExceptionState is AbortSignals) {
302
							log(String.Format("abort signal received: {0}",ex.ExceptionState));
303
							switch((AbortSignals)ex.ExceptionState) {
304
								default:
305
									Thread.ResetAbort();
306
									break;
307
							}
308
						}else {
309
							log(ex.Message);
310
							//throw(ex);
311
							// we just bypass everything else
312
						}
150
					}
313
					}
314
						
151
				}
315
				}
316
				scheduler_ready = false;
317
				scheduler = null;
318
				log("timer scheduelr is shutting down");
152
			}
319
			}
320
321
			void log(string str) {
322
				if (Environment.GetEnvironmentVariable("MONO_TIMER_DEBUG") != null)
323
					Console.Error.WriteLine(String.Format("{0} TIMER SCHEDULER: {1}",DateTime.Now,str));
324
			}
153
		}
325
		}
154
326
155
		Runner runner;
327
		// attributes
156
		AutoResetEvent start_event;
328
		// id used to reference this job in the timer-scheduler
157
		Thread t;
329
		int JobID;
158
330
159
		public Timer (TimerCallback callback, object state, int dueTime, int period)
331
		public Timer (TimerCallback callback, object state, int dueTime, int period)
160
		{
332
		{
Lines 198-209 Link Here
198
370
199
		void Init (TimerCallback callback, object state, int dueTime, int period)
371
		void Init (TimerCallback callback, object state, int dueTime, int period)
200
		{
372
		{
201
			start_event = new AutoResetEvent (false);
373
			TimerScheduler scheduler = TimerScheduler.GetInstance();
202
			runner = new Runner (callback, state, start_event);
374
			JobID = scheduler.AddJob(callback,state,dueTime,period);
203
			Change (dueTime, period);
204
			t = new Thread (new ThreadStart (runner.Start));
205
			t.IsBackground = true;
206
			t.Start ();
207
		}
375
		}
208
376
209
		public bool Change (int dueTime, int period)
377
		public bool Change (int dueTime, int period)
Lines 214-227 Link Here
214
			if (period < -1)
382
			if (period < -1)
215
				throw new ArgumentOutOfRangeException ("period");
383
				throw new ArgumentOutOfRangeException ("period");
216
384
217
			if (runner == null)
385
			TimerScheduler scheduler = TimerScheduler.GetInstance();
218
				return false;
386
			scheduler.ChangeJob(JobID,dueTime,period);
219
220
			start_event.Reset ();
221
			runner.Abort ();
222
			runner.DueTime = dueTime;
223
			runner.Period = period;
224
			start_event.Set ();
225
			return true;
387
			return true;
226
		}
388
		}
227
389
Lines 255-269 Link Here
255
417
256
		public void Dispose ()
418
		public void Dispose ()
257
		{
419
		{
258
			if (t != null && t.IsAlive) {
420
			TimerScheduler scheduler = TimerScheduler.GetInstance();
259
				if (t != Thread.CurrentThread)
421
			scheduler.RemoveJob(JobID);
260
					t.Abort ();
261
				t = null;
262
			}
263
			if (runner != null) {
264
				runner.Dispose ();
265
				runner = null;
266
			}
267
			GC.SuppressFinalize (this);
422
			GC.SuppressFinalize (this);
268
		}
423
		}
269
424
Lines 274-287 Link Here
274
			return true;
429
			return true;
275
		}
430
		}
276
431
277
		~Timer ()
278
		{
279
			if (t != null && t.IsAlive)
280
				t.Abort ();
281
282
			if (runner != null)
283
				runner.Abort ();
284
		}
285
	}
432
	}
286
}
433
}
287
434
(-)class/corlib/System.Threading/ChangeLog (+3 lines)
Lines 1-3 Link Here
1
2006-06-13 Rafael Ferreira <raf@ophion.org>
2
	* Timer.cs: Changed timer logic to use a single scheduler thread
3
1
2006-05-05  Sebastien Pouliot  <sebastien@ximian.com>
4
2006-05-05  Sebastien Pouliot  <sebastien@ximian.com>
2
5
3
	* ExecutionContext.cs: Don't capture the compressed stack unless the 
6
	* ExecutionContext.cs: Don't capture the compressed stack unless the 
(-)class/corlib/Test/System.Threading/TimerTest.cs (-51 / +37 lines)
Lines 23-132 Link Here
23
	// -- Ben
23
	// -- Ben
24
	//
24
	//
25
	public class TimerTest : Assertion {
25
	public class TimerTest : Assertion {
26
		// this bucket is used to avoid non-theadlocal issues
27
		class Bucket {
28
			public int count;
29
		}
30
		
26
		[Test]
31
		[Test]
27
		[Category ("NotWorking")]
28
		public void TestDueTime ()
32
		public void TestDueTime ()
29
		{
33
		{
30
			counter = 0;
34
			Bucket bucket = new Bucket();
31
			Timer t = new Timer (new TimerCallback (Callback), null, 200, Timeout.Infinite);
35
			Timer t = new Timer (new TimerCallback (Callback), bucket, 200, Timeout.Infinite);
32
			Thread.Sleep (50);
36
			Thread.Sleep (50);
33
			AssertEquals ("t0", 0, counter);
37
			AssertEquals ("t0", 0, bucket.count);
34
			Thread.Sleep (200);
38
			Thread.Sleep (200);
35
			AssertEquals ("t1", 1, counter);
39
			AssertEquals ("t1", 1, bucket.count);
36
			Thread.Sleep (500);
40
			Thread.Sleep (500);
37
			AssertEquals ("t2", 1, counter);
41
			AssertEquals ("t2", 1, bucket.count);
38
			
39
			t.Change (10, 10);
42
			t.Change (10, 10);
40
			Thread.Sleep (500);
43
			Thread.Sleep (1000);
41
			Assert ("t3", counter > 20);
44
			Assert ("t3", bucket.count > 20);
42
			t.Dispose ();
45
			t.Dispose ();
43
		}
46
		}
44
47
45
		[Test]
48
		[Test]
46
		[Category ("NotWorking")]
47
		public void TestChange ()
49
		public void TestChange ()
48
		{
50
		{
49
			counter = 0;
51
			Bucket bucket = new Bucket();
50
			Timer t = new Timer (new TimerCallback (Callback), null, 1, 1);
52
			Timer t = new Timer (new TimerCallback (Callback), bucket, 1, 1);
51
			Thread.Sleep (500);
53
			Thread.Sleep (500);
52
			int c = counter;
54
			int c = bucket.count;
53
			Assert ("t1", c > 20);
55
			Assert ("t1", c > 20);
54
			t.Change (100, 100);
56
			t.Change (100, 100);
55
			Thread.Sleep (500);
57
			Thread.Sleep (500);
56
			Assert ("t2", counter <= c + 6);
58
			Assert ("t2", bucket.count <= c + 6);
57
			t.Dispose ();
59
			t.Dispose ();
58
		}
60
		}
59
61
60
		[Test]
62
		[Test]
61
		[Category ("NotWorking")]
62
		public void TestZeroDueTime () {
63
		public void TestZeroDueTime () {
63
			counter = 0;
64
			Bucket bucket = new Bucket();
64
65
65
			Timer t = new Timer (new TimerCallback (Callback), null, 0, Timeout.Infinite);
66
			Timer t = new Timer (new TimerCallback (Callback), bucket, 0, Timeout.Infinite);
66
			Thread.Sleep (100);
67
			Thread.Sleep (100);
67
			AssertEquals (1, counter);
68
			AssertEquals (1, bucket.count);
68
			t.Change (0, Timeout.Infinite);
69
			t.Change (0, Timeout.Infinite);
69
			Thread.Sleep (100);
70
			Thread.Sleep (100);
70
			AssertEquals (2, counter);
71
			AssertEquals (2, bucket.count);
71
			t.Dispose ();
72
			t.Dispose ();
72
		}
73
		}
73
74
		[Test]
74
		[Test]
75
		[Category ("NotWorking")]
76
		public void TestDispose ()
75
		public void TestDispose ()
77
		{
76
		{	
78
			counter = 0;
77
			Bucket bucket = new Bucket();
79
			Timer t = new Timer (new TimerCallback (CallbackTestDispose), null, 10, 10);
78
			Timer t = new Timer (new TimerCallback (Callback), bucket, 10, 10);
80
			Thread.Sleep (200);
79
			Thread.Sleep (200);
81
			t.Dispose ();
80
			t.Dispose ();
82
			Thread.Sleep (20);
81
			Thread.Sleep (20);
83
			int c = counter;
82
			int c = bucket.count;
84
			Assert (counter > 5);
83
			Assert (bucket.count > 5);
85
			Thread.Sleep (200);
84
			Thread.Sleep (200);
86
			AssertEquals (c, counter);
85
			AssertEquals (c, bucket.count);
87
		}
86
		}
88
87
89
		[Test] // bug #78208
88
		[Test] // bug #78208
90
		public void TestDispose2 ()
89
		public void TestDispose2 ()
91
		{
90
		{
92
			Timer t = new Timer (new TimerCallback (CallbackTestDispose), null, 10, 10);
91
			Timer t = new Timer (new TimerCallback (Callback), null, 10, 10);
93
			t.Dispose ();
92
			t.Dispose ();
94
			t.Dispose ();
93
			t.Dispose ();
95
		}
94
		}
96
		
95
	
97
		[Test]
96
		[Test]
98
		[Category ("NotWorking")]
97
		[Category("NotWorking")]
99
		public void TestDisposeOnCallback () {
98
		public void TestDisposeOnCallback () {
100
			counter = 0;
99
		
101
			t1 = new Timer (new TimerCallback (CallbackTestDisposeOnCallback), null, 0, 10);
100
			Timer t1 = null;
101
			t1 = new Timer (new TimerCallback (CallbackTestDisposeOnCallback), t1, 0, 10);
102
			Thread.Sleep (200);
102
			Thread.Sleep (200);
103
			AssertNull (t1);
103
			AssertNull(t1);
104
			
104
			
105
			counter = 2;
106
			t1 = new Timer (new TimerCallback (CallbackTestDisposeOnCallback), null, 50, 0);
107
			Thread.Sleep (200);
108
			AssertNull (t1);
109
		}
105
		}
110
		
106
111
		private void CallbackTestDisposeOnCallback (object foo)
107
		private void CallbackTestDisposeOnCallback (object foo)
112
		{
108
		{
113
			if (++counter == 3) {
109
			((Timer)foo).Dispose();
114
				t1.Dispose ();
115
				t1 = null;
116
			}
117
		}
110
		}
118
111
119
		private void CallbackTestDispose (object foo)
120
		{
121
			counter++;
122
		}
123
124
		private void Callback (object foo)
112
		private void Callback (object foo)
125
		{
113
		{
126
			counter++;
114
			Bucket b = foo as Bucket;
115
			b.count++;
127
		}
116
		}
128
129
		Timer t1;
130
		int counter;
131
	}
117
	}
132
}
118
}
(-)class/corlib/Test/System.Threading/ChangeLog (+4 lines)
Lines 1-3 Link Here
1
2006-06-13 Rafael Ferreira <raf@ophion.org>
2
	* TimerTests.cs: Changed tests to use a counter object. Fixed tests with 
3
	category NotWorking - Only NotWorking left is TestDisposeOnCallback
4
1
2006-04-30  Gert Driesen  <drieseng@users.sourceforge.net>
5
2006-04-30  Gert Driesen  <drieseng@users.sourceforge.net>
2
6
3
	* TimerTest.cs: Added test for bug #78208. Marked individual tests
7
	* TimerTest.cs: Added test for bug #78208. Marked individual tests

Return to bug 315999