C# System.Threading.Timer 详解及示例

c#,system,threading,timer,详解,示例 · 浏览次数 : 3387

小编点评

**使用 System.Threading.Timer 类实现 SafeTimer 类** ** SafeTimer 类** - 考虑了 System.Threading.Timer 类抛出 ObjectDisposedExceptio n异常的问题。 - 使用 ManualResetEvent 实现了对 Timer 的多次 disposal 的安全。 - 加入锁机制来防止 Timer.Dispose 方法被多次重复调用。 - 使用 Try/Catch块来捕获可能在 Timer.Dispose 方法执行过程中抛出的 ObjectDisposedExceptio n异常。 - 在 Dispose 方法中释放锁,以防止资源泄漏。 - 在 Dispose 方法中返回 false,以表示已完成 disposal。 ** 代码示例** ```csharp using System; using System.Threading; namespace TimerDispose { class SafeTimer { private readonly TimeSpan _disposalTimeout; private readonly System.Threading.Timer _timer; private bool _disposeEnded; public SafeTimer(TimeSpan disposalTimeout) { _disposalTimeout = disposalTimeout; _timer = new System.Threading.Timer(HandleTimerElapsed); } public void TriggerOnceIn(TimeSpan time) { try { _timer.Change(time, Timeout.InfiniteTimeSpan); } catch (ObjectDisposedException) { if (_disposeEnded) { // we still want to throw the exception in case someone really tries // to change the timer after disposal has finished throw; } } } public void Dispose() { using (var waitHandle = new ManualResetEvent(false)) { // returns false on second dispose if (_timer.Dispose(waitHandle)) { if (!waitHandle.WaitOne(_disposalTimeout)) { throw new TimeoutException( "Timeout waiting for timer to stop. (...)" ); } _disposeEnded = true; } } } } } ``` **其他优化** - 使用异步方法来实现 SafeTimer 的事件处理。 - 使用委托或异步回调来处理 SafeTimer 的事件。 - 使用缓存机制来优化 SafeTimer 的创建和 disposal。

正文

前言

定时器功能在日常开发中也是比较常用的,在 .Net 中实际上总共有五种定时器,分别是:System.Timers.TimerSystem.Threading.TimerSystem.Windows.Forms.TimerSystem.Web.UI.Timer (仅 .NET Framework)、System.Windows.Threading.DispatcherTimer

其中最常用的就是 System.Threading.Timer 基于线程池的定时器,相较于另外几种定时器,其安全性较高,适用性最强,因此本文将详细介绍此定时器的相关内容。

一、两类重载

参考:Timer 构造函数

1、 Timer(TimerCallback)

使用新创建的 Timer 对象作为状态对象,用一个无限周期和一个无限到期时间初始化 Timer 类的新实例。当循环任务达成时,可以在回调函数中将当前的 Timer 对象释放掉。

// 语法
public Timer (System.Threading.TimerCallback callback);

下面是一个简单示例:(在回调函数 TimerProc 中,我们可以通过将 Timer 对象释放掉,来结束循环过程)

using System;
using System.Threading;

namespace Test.Test.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Program ex = new Program();
            ex.StartTimer(4000); // 创建两个 Timer 对象
            ex.StartTimer(1000);
            Console.WriteLine("Press Enter to end the program.");
            Console.ReadLine();
        }
        public void StartTimer(int dueTime)
        {
            Timer t = new Timer(new TimerCallback(TimerProc));
            t.Change(dueTime, 5000); // 启动定时器
            // dueTime:表示延迟调用的时间;
            // 5000:表示回调的时间间隔,单位:毫秒
            // 如果在回调中将 Timer 释放掉,则后续回调将无法发生
        }
        private void TimerProc(object state) // 入参对象为 Timer 对象
        {
            Timer t = (Timer)state;
            t.Dispose(); // 调用一次就释放掉,或者添加条件释放
            Console.WriteLine("The timer callback executes.");
        }
    }
}
// 输出结果:
// Press Enter to end the program.
// The timer callback executes.
// The timer callback executes.

2、Timer(TimerCallback, Object, Int32, Int32)

使用 32 位的有符号整数指定时间间隔,初始化 Timer 类的新实例。callback:回调函数名;state:包含回调方法要使用的信息的状态对象,可为空;dueTime:延迟调用的时间;period:重复回调的时间间隔。

// 语法
public Timer (System.Threading.TimerCallback callback, object? state, int dueTime, int period);

下面是一个示例:(关于线程自动重置类:AutoResetEvent 类

点击查看代码 通过线程同步事件,演示不同时间间隔输出结果的区别
using System;
using System.Threading;

class Program
{
    static void Main()
    {
        // AutoResetEvent:表示一个线程同步事件,在等待线程释放后,收到信号时,自动重置
        var autoEvent = new AutoResetEvent(false);
        var statusChecker = new StatusChecker(10); // 实例化 StatusChecker 并设置最大循环次数
        Console.WriteLine($"{DateTime.Now.ToString("HH:mm:dd.fff")} Creating timer.\n");
        var stateTimer = new Timer(statusChecker.CheckStatus, autoEvent, 1000, 250);
        // 1000 表示延迟 1s 开始执行;250 表示回调时间间隔为 0.25s
        autoEvent.WaitOne(); // WaitOne:阻塞当前线程,直到 WaitHandle 接收到信号
        stateTimer.Change(0, 500); // Change:0 不延迟立即启动执行;500 表示回调间隔为 0.5s
        Console.WriteLine("\nChanging period to .5 seconds.\n");
        autoEvent.WaitOne(); // WaitOne:阻塞当前线程,直到 WaitHandle 接收到信号
        stateTimer.Dispose(); // 释放 Timer 对象
        Console.WriteLine("\nDestroying timer.");
    }
}
class StatusChecker
{
    private int invokeCount; // 回调计数
    private int maxCount; // 回调最大次数
    public StatusChecker(int count)
    {
        invokeCount = 0;
        maxCount = count;
    }
    public void CheckStatus(Object stateInfo)
    {
        AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
        Console.WriteLine($"{DateTime.Now.ToString("HH: mm:ss.fff")} Checking status {(++invokeCount)}.");
        if (invokeCount == maxCount)
        {
            invokeCount = 0;
            // Set 将事件状态设置为有信号状态,允许一个或多个等待线程继续执行
            autoEvent.Set();
        }
    }
}
// 输出结果:
// 11:26:22.571 Creating timer.
// 
// 11:26:16.489 Checking status  1.
// 11:26:16.500 Checking status  2.
// 11:26:16.749 Checking status  3.
// 11:26:17.000 Checking status  4.
// 11:26:17.245 Checking status  5.
// 11:26:17.503 Checking status  6.
// 11:26:17.744 Checking status  7.
// 11:26:17.993 Checking status  8.
// 11:26:18.241 Checking status  9.
// 11:26:18.504 Checking status 10.
// 
// Changing period to .5 seconds.
// 
// 11:26:18.505 Checking status  1.
// 11:26:19.017 Checking status  2.
// 11:26:19.510 Checking status  3.
// 11:26:20.009 Checking status  4.
// 11:26:20.509 Checking status  5.
// 11:26:21.020 Checking status  6.
// 11:26:21.518 Checking status  7.
// 11:26:22.016 Checking status  8.
// 11:26:22.516 Checking status  9.
// 11:26:23.016 Checking status 10.
// 
// Destroying timer.

与此重载类似用法的另外三个重载如下:

// 1、用 64 位整数表示时间间隔
Timer(TimerCallback, Object, Int64, Int64)
// 2、时间戳参数
//  时间戳语法:public TimeSpan (int hours, int minutes, int seconds);
//  TimeSpan delayTime = new TimeSpan(0, 0, 1); 
//  时间戳语法:public TimeSpan (int days, int hours, int minutes, int seconds, int milliseconds, int microseconds);
//  TimeSpan intervalTime = new TimeSpan(0, 0, 0, 0, 250);
Timer(TimerCallback, Object, TimeSpan, TimeSpan)
// 3、用 32 位无符号整数来表示时间间隔
Timer(TimerCallback, Object, UInt32, UInt32)

二、属性 ActiveCount

获取当前活动的计时器数量。 活动计数器定义为,在未来某一时间点触发且尚未取消。

下面是一个简单的示例:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        var stateTimer = new Timer((para)=>{ }, null, 1000, 250);
        var stateTimer2 = new Timer((para) => { }, null, 1000, 250);
        Console.WriteLine($"1、Timer ActiveCount.{Timer.ActiveCount}");
        stateTimer.Dispose(); // 释放 Timer 对象
        Console.WriteLine($"\n2、Timer ActiveCount.{Timer.ActiveCount}");
        stateTimer2.Dispose(); // 释放 Timer2 对象
        Console.WriteLine($"\n3、Timer ActiveCount.{Timer.ActiveCount}");
        Thread.Sleep(5000);
    }
}
// 输出结果:
// 1、Timer ActiveCount.2
// 
// 2、Timer ActiveCount.1
// 
// 3、Timer ActiveCount.0

三、方法

1、Timer.Change 方法

更改计时器的延迟启动时间和方法循环调用之间的时间间隔。单位均为毫秒(ms)。

其和 Timer 的构造函数重载类似,都是有四个重载,之间只有参数不同,用法相同。

四种类型分别是:Int32(32 位正整数)、Int64(64 位正整数)、TimeSpan(时间戳)、UInt32(32 位无符号整数)。

下面例举一个时间为正整数的示例:

// 先创建一个 Timer 对象
var stateTimer = new Timer((para)=>{ }, null, 1000, 250);
// 调用变更对象的方法如下:1000 表示:延迟 1s 触发;500 表示间隔 0.5s 循环调用
stateTimer.Change(1000, 500);

2、Timer.Dispose 方法

此方法共有两个重载,分别是:Dispose()、Dispose(WaitHandle)。

Dispose()	
// 释放由 Timer 实例使用的当前所有资源
Dispose(WaitHandle)	
// 释放由 Timer 实例使用的当前所有资源,并在释放完成时发出信号

Dispose() 方法就是直接将 Timer 对象释放调,这里就不再赘述了,下面来看一个关于 Dispose(WaitHandle) 的示例:

using System;
using System.Threading;

namespace TimerDispose
{
    class Program
    {
        static Timer timer = null; //**声明一个全局变量,避免 Timer 对象后续没有调用时,被 GC回收
        // ManualResetEvent 继承自 WaitHandle
        // 是否手动重置事件(是 WaitHandle 的子类) false 初始状态为无信号 true 初始状态为有信号
        static ManualResetEvent timerDisposed = null;
        static void CreateAndStartTimer()
        {
            // 初始化 Timer,设置触发间隔为 2000 毫秒,设置 dueTime 参数为 Timeout.Infinite 表示不启动 Timer
            timer = new Timer(TimerCallBack, null, Timeout.Infinite, 2000);
            // 启动 Timer,设置 dueTime 参数为 0 表示立刻启动 Timer
            //**先实例化再启动的目的是:避免在调用 Dispose 方法前,timer 对象还未完成赋值,所导致的空对象错误
            timer.Change(0, 2000);
        }
        /// <summary>
        /// TimerCallBack 方法是 Timer 每一次触发后的事件处理方法
        /// </summary>
        static void TimerCallBack(object state)
        {
            // Thread.Sleep(10000); // Change() 报错:System.ObjectDisposedException: 'Cannot access a disposed object.'
            try
            {
                timer.Change(0, 1000);
            }
            catch (ObjectDisposedException) //**当 Timer 对象已经调用了 Dispose 方法后,再调用 Change 方法,会抛出 ObjectDisposedException 异常
            {
                Console.WriteLine("在 Timer.Dispose 方法执行后,再调用 Timer.Change 方法已经没有意义");
            }
            Thread.Sleep(10000); // 在 Change() 之后可正常运行
        }
        static void Main(string[] args)
        {
            CreateAndStartTimer();
            Console.WriteLine("按任意键调用Timer.Dispose方法...");
            Console.ReadKey();
            timerDisposed = new ManualResetEvent(false);
            // 调用 Timer 的 bool Dispose(WaitHandle notifyObject) 重载方法,来结束Timer的触发,
            // 当线程池中的所有 TimerCallBack 方法都执行完毕后,Timer 会发一个信号给 timerDisposed
            timer.Dispose(timerDisposed);
            // WaitHandle.WaitOne() 方法会等待收到一个信号,否则一直被阻塞
            timerDisposed.WaitOne();
            timerDisposed.Dispose();
            Console.WriteLine("Timer已经结束,按任意键结束整个程序...");
            Console.ReadKey();
        }
    }
}

另外一个很不错的示例,是一个国外的高手所写,不仅考虑到了 Timer.Change 方法会抛出 ObjectDisposedExceptio n异常,他还给 WaitHandle.WaitOne 方法添加了超时限制(_disposalTimeout),并且还加入了逻辑来防止 Timer.Dispose 方法被多次重复调用,注意 Timer 的 bool Dispose(WaitHandle notifyObject) 重载方法是会返回一个 bool 值的,如果它返回了 false,那么表示 Timer.Dispose 方法已经被调用过了,代码如下所示:

点击查看代码 一个更优秀的示例代码
using System;
using System.Threading;

namespace TimerDispose
{
    class SafeTimer
    {
        private readonly TimeSpan _disposalTimeout;

        private readonly System.Threading.Timer _timer;

        private bool _disposeEnded;

        public SafeTimer(TimeSpan disposalTimeout)
        {
            _disposalTimeout = disposalTimeout;
            _timer = new System.Threading.Timer(HandleTimerElapsed);
        }

        public void TriggerOnceIn(TimeSpan time)
        {
            try
            {
                _timer.Change(time, Timeout.InfiniteTimeSpan);
            }
            catch (ObjectDisposedException)
            {
                // race condition with Dispose can cause trigger to be called when underlying
                // timer is being disposed - and a change will fail in this case.
                // see 
                // https://msdn.microsoft.com/en-us/library/b97tkt95(v=vs.110).aspx#Anchor_2
                if (_disposeEnded)
                {
                    // we still want to throw the exception in case someone really tries
                    // to change the timer after disposal has finished
                    // of course there's a slight race condition here where we might not
                    // throw even though disposal is already done.
                    // since the offending code would most likely already be "failing"
                    // unreliably i personally can live with increasing the
                    // "unreliable failure" time-window slightly
                    throw;
                }
            }
        }

        //Timer每一次触发后的事件处理方法
        private void HandleTimerElapsed(object state)
        {
            //Do something
        }

        public void Dispose()
        {
            using (var waitHandle = new ManualResetEvent(false))
            {
                // returns false on second dispose
                if (_timer.Dispose(waitHandle))
                {
                    if (!waitHandle.WaitOne(_disposalTimeout))
                    {
                        throw new TimeoutException(
                            "Timeout waiting for timer to stop. (...)");
                    }
                    _disposeEnded = true;
                }
            }
        }
    }
}

参考:System.Threading.Timer如何正确地被Dispose

3、Timer.DisposeAsync 方法

其为上一部分的一种异步实现。

从 .NET Core 开始,就意味着 .NET 来到了一个全新的异步时代。无论是各种基础类库(比如 System.IO)、AspNet Core、还是 EFCore 等等,它们都逐渐支持异步操作。其不阻止线程的执行,带来高性能的同时还基本不需要更改原有的编码习惯,因此后续针对异步的编程肯定会越来越普遍。

当一个实体类同时实现了 Dispose 和 DisposeAsync,由于程序会先判断时候实现了 DisposeAsync 异步释放,所以一般优先调用异步释放。参考:熟悉而陌生的新朋友——IAsyncDisposable

终极参考:Timer 类

与C# System.Threading.Timer 详解及示例相似的内容:

C# System.Threading.Timer 详解及示例

System.Threading.Timer 基于线程池的定时器,相较于另外几种定时器,其安全性较高,适用性最强,因此本文通过重载、属性、方法等方面介绍此定时器的相关内容。

【Android 逆向】【攻防世界】easyjni

1. apk 安装到手机,提示需要输入flag 2. jadx打开apk public class MainActivity extends c { static { System.loadLibrary("native"); } /* JADX INFO: Access modifiers cha

像go 一样 打造.NET 单文件应用程序的编译器项目bflat 发布 7.0版本

现代.NET和C#在低级/系统程序以及与C/C++/Rust等互操作方面的能力完全令各位刮目相看了,有人用C#开发的64位操作系统: GitHub - nifanfa/MOOS: C# x64 operating system pro...,截图要介绍的是一个结合Roslyn和NativeAOT的实

C#.Net筑基-类型系统①基础

C#.Net的BCL提供了丰富的类型,最基础的是值类型、引用类型,而他们的共同(隐私)祖先是 System.Object(万物之源),所以任何类型都可以转换为Object。

C# 使用SqlDataAdapter和DataSet来访问数据库

使用SqlDataAdapter和DataSet来访问数据库 报:已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭 解决方法,使用 using 包住 connection using System; using System.Data; using System.Da

WPF/C#:程序关闭的三种模式

ShutdownMode枚举类型介绍 ShutdownMode是一个枚举类型,它定义了WPF应用程序的关闭方式。这个枚举类型有三个成员: OnLastWindowClose:当最后一个窗口关闭或者调用System.Windows.Application.Shutdown方法时,应用程序会关闭。 On

[转帖]wmic命令介绍

https://www.jianshu.com/p/3e1a5a8fa23b How to Get Your System Serial Number PS C:\windows\system32> wmic bios get serialnumber SerialNumber SN-1 How t

【解惑】当处理同一个字段的并发问题时,使用乐观锁来处理库存数量

以下是一个使用乐观锁处理库存数量并发问题的c#示例代码: ```csharp using System; using System.Data; using System.Data.SqlClient; public class InventoryService { private string co

UnityShader数学基础篇

Mathf Mathf和Math 1、Math是C#中封装好的用于数学计算的工具类,位于System命名空间中。 2、Mathf是Unity中封装好的用于数学计算的工具结构体,位于UnityEngine命名空间中。 Mathf中的常用方法 1.π - PI print(Mathf.PI); 2.取绝

自己制作AM启动方式,不需要每次输入密码和用户名

第一布,查看用户名,数据库等信息 在记事本中写以下信息,保存后,后缀改为bat,双击此文件即可启动hull design模块且无黑框框的控制台哦 C:\AVEVA\Marine\OH12.1.SP4\marine.bat noconsole Mar SYSTEM/XXXXXX/PLANARHULL