测试发现 Task.Delay 计时精度不够,C#中论计时,还是 Stopwatch 最精确,由于要切换线程,还要考虑创建线程消耗的时间,果断使用线程池,最后实现的代码如下:
class TaskUtil
{
/// <summary>
/// 发出一个可取消的超时任务
/// </summary>
/// <param name="delay">超时时间</param>
/// <param name="cts">用于超时前取消任务 cts.Cancel() </param>
/// <param name="action">超时后执行的任务</param>
//[MethodImpl(MethodImplOptions.AggressiveInlining)]//方法内联
public static void PostCancelableAction(long delay, CancellationTokenSource cts, Action action)
{
//ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
//Print($"workerThreads={workerThreads}, completionPortThreads={completionPortThreads}");
Print("启动任务");
Stopwatch sw = Stopwatch.StartNew();
//使用线程池能够减少切换线程的耗时(2ms左右)
ThreadPool.QueueUserWorkItem((state) => {
long timeDiff = sw.ElapsedMilliseconds;
Print($"执行任务Action 启动耗时{timeDiff}ms");
long timeout = delay - timeDiff;
while (!cts.IsCancellationRequested)
{
if (sw.ElapsedMilliseconds >= timeout) break;
}
sw.Stop();
if (cts.IsCancellationRequested)
{
Print("取消任务");
}
else
{
Print("任务超时");
action?.Invoke();
}
});
}
public static async void Delay(int delay)
{
Print("Before delay");
await Task.Delay(delay);
Print("After delay");
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]//方法内联
public static void Print(object msg)
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - {msg}");
}
}
测试代码
TaskUtil.Print("Form1() - Before PostCancelableAction");
CancellationTokenSource cts = new CancellationTokenSource();
TaskUtil.PostCancelableAction(5, cts, () => {
TaskUtil.Print("Form1() - 执行超时任务");
});
//cts.Cancel();
TaskUtil.Print("Form1() - After PostCancelableAction");
另外,如果要计时精度高的定时器,可以使用 winmm.dll 中的函数,代码(原始链接)如下
public class MMTimer : IDisposable
{
//Lib API declarations
[DllImport("Winmm.dll", CharSet = CharSet.Auto)]
static extern uint timeSetEvent(uint uDelay, uint uResolution, TimerCallback lpTimeProc, UIntPtr dwUser, uint fuEvent);
[DllImport("Winmm.dll", CharSet = CharSet.Auto)]
static extern uint timeKillEvent(uint uTimerID);
[DllImport("Winmm.dll", CharSet = CharSet.Auto)]
static extern uint timeGetTime();
[DllImport("Winmm.dll", CharSet = CharSet.Auto)]
static extern uint timeBeginPeriod(uint uPeriod);
[DllImport("Winmm.dll", CharSet = CharSet.Auto)]
static extern uint timeEndPeriod(uint uPeriod);
//Timer type definitions
[Flags]
public enum fuEvent : uint
{
TIME_ONESHOT = 0, //Event occurs once, after uDelay milliseconds.
TIME_PERIODIC = 1,
TIME_CALLBACK_FUNCTION = 0x0000, /* callback is function */
//TIME_CALLBACK_EVENT_SET = 0x0010, /* callback is event - use SetEvent */
//TIME_CALLBACK_EVENT_PULSE = 0x0020 /* callback is event - use PulseEvent */
}
//Delegate definition for the API callback
delegate void TimerCallback(uint uTimerID, uint uMsg, UIntPtr dwUser, UIntPtr dw1, UIntPtr dw2);
//IDisposable code
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
Stop();
}
}
disposed = true;
}
~MMTimer()
{
Dispose(false);
}
/// <summary>
/// The current timer instance ID
/// </summary>
uint id = 0;
/// <summary>
/// The callback used by the the API
/// </summary>
TimerCallback thisCB;
/// <summary>
/// The timer elapsed event
/// </summary>
public event EventHandler Timer;
protected virtual void OnTimer(EventArgs e)
{
if (Timer != null)
Timer(this, e);
}
public MMTimer()
{
//Initialize the API callback
thisCB = CBFunc;
}
/// <summary>
/// Stop the current timer instance (if any)
/// </summary>
public void Stop()
{
lock (this)
{
if (id != 0)
{
timeKillEvent(id);
Debug.WriteLine("MMTimer " + id.ToString() + " stopped");
id = 0;
}
}
}
/// <summary>
/// Start a timer instance
/// </summary>
/// <param name="ms">Timer interval in milliseconds</param>
/// <param name="repeat">If true sets a repetitive event, otherwise sets a one-shot</param>
public void Start(uint ms, bool repeat)
{
//Kill any existing timer
Stop();
//Set the timer type flags
fuEvent f = fuEvent.TIME_CALLBACK_FUNCTION | (repeat ? fuEvent.TIME_PERIODIC : fuEvent.TIME_ONESHOT);
lock (this)
{
id = timeSetEvent(ms, 0, thisCB, UIntPtr.Zero, (uint)f);
if (id == 0)
throw new Exception("timeSetEvent error");
Debug.WriteLine("MMTimer " + id.ToString() + " started");
}
}
void CBFunc(uint uTimerID, uint uMsg, UIntPtr dwUser, UIntPtr dw1, UIntPtr dw2)
{
//Callback from the MMTimer API that fires the Timer event. Note we are in a different thread here
OnTimer(new EventArgs());
}
}