在多线程专栏中编写了 C# 定时器相关知识,但是内容不是很完善,近期加深了一些认识,顺便做一下笔记。
https://threads.whuanle.cn/1.thread_basic/3.pool.html#%E8%AE%A1%E6%97%B6%E5%99%A8
这里不提桌面里面的定时器,只讨论 .NET 本身的定时器,有:
System.Threading.Timer
System.Timers.Timer
PeriodicTimer
System.Threading.Timer 是 .NET 中的基础定时器,其它定时器都是基于此做的封装。
System.Threading.Timer 的误差大概是按毫秒计的。
不同 .NET 版本、不同操作系统、不同 CPU 可能有差异。
其构造函数如下:
// TimerCallback:要被执行的函数
// state:传递的对象
// dueTime:延迟执行时间,即应该什么时候开始执行
// period:多久执行一次
Timer(TimerCallback callback, object? state, int dueTime, int period)
如下代码所所示,立即启动一个定时器,按照每秒的间隔执行一个函数:
void Main()
{
using Timer timer = new Timer(WriteTime!, null, 0, 1000);
Console.ReadLine();
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
或者这样写:
void Main()
{
using Timer timer = new Timer(WriteTime);
timer.Change(0, 1000);
Console.ReadLine();
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
可以看到
因为 System.Threading.Timer 实现了 IDisposable, IAsyncDisposable,所以记得使用 using 或者手动调用 Dispose 函数释放定时器,否则容易出现内存泄露。
另外,不一定创建构造函数立即执行,可以手动调用 Change 函数设置开始执行。
同等写法:
void Main()
{
using Timer timer = new Timer(WriteTime!);
timer.Change(0, 1000);
Console.ReadLine();
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
如果不希望被执行,可以把延迟时间设置为 -1
,这样该定时器就一直不会被执行。
using Timer timer = new Timer(WriteTime!, null, -1, 1000);
如果要在合适的时候执行,可以先设置 延迟时间 -1
,然后直到出现 Change()
函数重新修改定时器执行间隔。
void Main()
{
using Timer timer = new Timer(WriteTime!, null, -1, 1000);
Console.WriteLine("按下回车键开始执行");
Console.ReadLine();
timer.Change(0, 1000);
Console.ReadLine();
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
如果要在执行期间动态修改修改下次执行的时间间隔,设置延迟的时间,但是不要设置定时间隔。
例如,下面的三个示例都会爆炸:
Timer timer;
void Main()
{
timer = new Timer(WriteTime, null, -1, 1000);
Console.ReadLine();
}
int time = 1000;
void WriteTime(object data)
{
time = time + 1000;
timer.Change(0, time);
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
Timer timer;
void Main()
{
timer = new Timer(WriteTime, null, 0, 1000);
timer.Change(0, 1000);
Console.ReadLine();
}
int time = 1000;
void WriteTime(object data)
{
time = time + 1000;
timer.Change(0, time);
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
Timer timer;
void Main()
{
timer = new Timer(WriteTime);
timer.Change(0, 1000);
Console.ReadLine();
}
int time = 1000;
void WriteTime(object data)
{
time = time + 1000;
timer.Change(0, time);
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
正确做法是忽略定时执行时间,设置延迟时间:
Timer timer;
void Main()
{
timer = new Timer(WriteTime);
timer.Change(0, 1000);
Console.ReadLine();
}
int time = 1000;
void WriteTime(object data)
{
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
timer.Change(time, 0);
}
例如每次执行时间都会多延迟 1 秒:
Timer timer;
void Main()
{
timer = new Timer(WriteTime);
timer.Change(0, -1);
Console.ReadLine();
}
int time = 1000;
void WriteTime(object data)
{
time = time + 1000;
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
timer.Change(time, -1);
}
时间间隔可以改成
-1
也可以写成0
。
System.Timers.Timer 则是对 System.Threading.Timer 的包装,使用上方便一些。
主要表现在:
Interval
替代了Change()
方法;Elapsed
绑定事件,可以绑定多个,而不是构造函数绑定;AutoReset
重置定时器;
System.Timers.Timer 在使用上更加简单,更加容易理解。
下面是示例,每秒执行一次函数:
void Main()
{
var timer = new System.Timers.Timer()
{
Interval = 1000
};
timer.Elapsed += WriteTime;
timer.Start();
Console.ReadLine();
timer.Stop();
}
void WriteTime(object? sender, ElapsedEventArgs e)
{
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
可以很简单启动停止定时器,可以随时绑定和解绑事件。
要注意,因为计时器是有单独线程处理的,所以如果不使用 Dispose 手动释放或者 使用 using 包裹,那么当前计时器就会后台执行,而不会被垃圾回收器回收。
如下面代码所示,因为是在堆栈内声明的定时器对象,所以执行垃圾回收后,定时器也会被回收,观察效果,会发现 WriteTime
不会被执行。
void Main()
{
var timer = new System.Threading.Timer(WriteTime, null, 0, 1000);
GC.Collect();
Console.ReadLine();
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
但是使用以下代码,垃圾回收器不会回收定时器,所以还会一直打印。
System.Threading.Timer timer;
void Main()
{
timer = new System.Threading.Timer(WriteTime, null, 0, 1000);
GC.Collect();
Console.ReadLine();
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
同样的方式,以下代码会导致内存泄露:
void Main()
{
Test test = new();
GC.Collect();
Console.ReadLine();
}
public class Test
{
System.Threading.Timer timer;
public Test()
{
timer = new System.Threading.Timer(WriteTime, null, 0, 1000);
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
所以很有必要避免内存泄露。
void Main()
{
using (Test test = new())
{
GC.Collect();
}
Console.ReadLine();
}
public class Test : System.IDisposable
{
System.Threading.Timer timer;
public Test()
{
timer = new System.Threading.Timer(WriteTime, null, 0, 1000);
}
void WriteTime(object data) =>
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
public void Dispose()
{
Console.WriteLine("Dispose");
timer.Dispose();
}
~Test()
{
Console.WriteLine("~Test");
timer.Dispose();
}
}
PeriodicTimer 的使用跟线程同步有关系。
例如说,定时器触发函数后,因为函数在另一个线程中执行,无法知道函数什么时候执行完毕,如何保证 1s 刚刚好执行一次这个函数?
我们可以这样写:
PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
void Main()
{
RepeatForEver();
Console.ReadLine();
timer.Dispose();
}
async void RepeatForEver()
{
while (await timer.WaitForNextTickAsync())
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
}
因为使用了 async void
,所以 RepeatForEver
是异步非阻塞的,通过 timer.WaitForNextTickAsync()
,定时器可以评估执行时间,等执行完毕后,确保每次触发时间都是 1s。
如下代码,虽然增加了等待时间 100ms,但是定时器会自动弥补时间差异,不会等待 1s 才会执行,而是大约 900ms 后执行,保证刚刚好去掉函数执行耗时,每秒执行一次函数。
PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
void Main()
{
RepeatForEver();
Console.ReadLine();
timer.Dispose();
}
async void RepeatForEver()
{
while (await timer.WaitForNextTickAsync())
{
Console.WriteLine(DateTime.Now.ToString("ss.fff"));
await Task.Delay(100);
}
}
文章评论