C# 定时器知识

2025年1月3日 102点热度 0人点赞 0条评论
内容目录

在多线程专栏中编写了 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"));

可以看到

file

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

file

痴者工良

高级程序员劝退师

文章评论