首页 > 其他分享 >揭秘 .NET 中的 TimerQueue

揭秘 .NET 中的 TimerQueue

时间:2023-08-10 16:37:46浏览次数:33  
标签:定时器 AutoResetEvent 到期 TimerQueue 线程 NET 揭秘 WaitOne

TimerQueue 与 OS 定时器的交互
按需注册定时器
TimerQueue 向 OS 注册定时器的过程被封装在 TimerQueueTimer 的 EnsureTimerFiresBy 方法中。有两处地方会调用 EnsureTimerFiresBy 方法

UpdateTimer 方法,此方法用于注册或更新 TimerQueueTimer。

FireNextTimers 方法中,此方法用于遍历和执行 TimerQueue 中的 TimerQueueTimer。如果遍历完所有到期的 TimerQueueTimer 后,发现
TimerQueue 中还有未到期的
TimerQueueTimer,那么会调用 EnsureTimerFiresBy 方法,保证后面到期的 TimerQueueTimer 能够被及时执行。

internal class TimerQueue : IThreadPoolWorkItem
{
private bool _isTimerScheduled;
private long _currentTimerStartTicks;
private uint _currentTimerDuration;

private bool EnsureTimerFiresBy(uint requestedDuration)
{
    // TimerQueue 会将 requestedDuration 限制在 0x0fffffff 以内
    // 0x0fffffff = 268435455 = 0x0fffffff / 1000 / 60 / 60 / 24 = 3.11 天
    // 也就是说,runtime 会将 requestedDuration 限制在 3.11 天以内
    // 因为 runtime 的定时器实现对于很长时间的定时器不太好用
    // OS 的定时器可能会提前触发,但是这没关系,TimerQueue 会检查定时器是否到期,如果没有到期,TimerQueue 会重新注册定时器
    const uint maxPossibleDuration = 0x0fffffff;
    uint actualDuration = Math.Min(requestedDuration, maxPossibleDuration);

    if (_isTimerScheduled)
    {
        long elapsed = TickCount64 - _currentTimerStartTicks;
        if (elapsed >= _currentTimerDuration)
            return true; // 当前定时器已经到期,不需要重新注册定时器

        uint remainingDuration = _currentTimerDuration - (uint)elapsed;
        if (actualDuration >= remainingDuration)
            return true; // 当前定时器的到期时间早于 requestedDuration,不需要重新注册定时器
    }

    // 注册定时器
    if (SetTimer(actualDuration))
    {
        _isTimerScheduled = true;
        _currentTimerStartTicks = TickCount64;
        _currentTimerDuration = actualDuration;
        return true;
    }

    return false;
}

}
在 EnsureTimerFiresBy 方法中,会记录当前 TimerQueue 的到期时间和状态,按需判断是否需要重新注册定时器。

AutoResetEvent 封装 OS 定时器
在进一步介绍 TimerQueue 是如何与 OS 的定时器进行交互之前,我们先来看一下 AutoResetEvent。

TimerQueue 使用了一个 AutoResetEvent 来等待定时器到期,封装了和 OS 定时器的交互。

AutoResetEvent 是一个线程同步的基元,它封装了一个内核对象,这个内核对象的状态有两种:终止状态和非终止状态,通过构造函数的 initialState 参数指定。

当调用 AutoResetEvent.WaitOne() 时,如果 AutoResetEvent 的状态为非终止状态,那么当前线程会被阻塞,直到 AutoResetEvent 的状态变为终止状态。

当调用 AutoResetEvent.Set() 时,如果 AutoResetEvent 的状态为非终止状态,那么 AutoResetEvent 的状态会变为终止状态,并且会唤醒一个等待的线程。

当调用 AutoResetEvent.Set() 时,如果 AutoResetEvent 的状态为终止状态,那么 AutoResetEvent 的状态不会发生变化,也不会唤醒等待的线程。

// 初始化为非终止状态,调用 WaitOne 会被阻塞
var autoResetEvent = new AutoResetEvent(initialState: false);
Task.Run(() =>
{
Console.WriteLine($"Task start {DateTime.Now:HH:mm:ss.fff}");
// 等待 Set 方法的调用,将 AutoResetEvent 的状态变为终止状态
autoResetEvent.WaitOne();
Console.WriteLine($"WaitOne1 end {DateTime.Now:HH:mm:ss.fff}");
// 每次被唤醒后,都会重新进入阻塞状态,等待下一次的唤醒
autoResetEvent.WaitOne();
Console.WriteLine($"WaitOne2 end {DateTime.Now:HH:mm:ss.fff}");
});

Thread.Sleep(1000);
autoResetEvent.Set();
Thread.Sleep(2000);
autoResetEvent.Set();

Console.ReadLine();
输出结果如下

Task start 10:42:39.914
WaitOne1 end 10:42:40.916
WaitOne2 end 10:42:42.918
同时,AutoResetEvent 还提供了 WaitOne 方法的重载,可以指定等待的时间。如果在指定的时间内,AutoResetEvent 的状态没有变为终止状态,那么 WaitOne 停止等待,唤醒线程。

public virtual bool WaitOne(TimeSpan timeout)
public virtual bool WaitOne(int millisecondsTimeout)
var autoResetEvent = new AutoResetEvent(false);
Task.Run(() =>
{
Console.WriteLine($"Task start {DateTime.Now:HH:mm:ss.fff}");
// 虽然 Set 方法在 2 秒后执行,但因为 WaitOne 方法的超时时间为 1 秒,所以 1 秒后就会执行下面的代码
autoResetEvent.WaitOne(TimeSpan.FromSeconds(1));
Console.WriteLine($"Task end {DateTime.Now:HH:mm:ss.fff}");
});

Thread.Sleep(2000);
autoResetEvent.Set();

Console.ReadLine();
输出结果如下

Task start 10:51:36.412
Task end 10:51:37.600
定时任务的管理
接下来我们看一下 SetTimer 方法的实现。

我们一共需要关注下面三个方法

SetTimer:用于注册定时器
InitializeScheduledTimerManager_Locked:只会被调用一次,用于初始化 TimerQueue 的定时器管理器,主要是初始化 TimerThread。
TimerThread:用于处理 OS 定时器到期的线程,所有的 TimerQueue 共用一个 TimerThread。TimerThread 会在 OS 定时器到期时被唤醒,然后会遍历所有的 TimerQueue,找到到期的 TimerQueue,然后将到期的 TimerQueue 放入到线程池中执行。
// TimerQueue 实现了 IThreadPoolWorkItem 接口,这意味着 TimerQueue 可以被放入到线程池中执行
internal class TimerQueue : IThreadPoolWorkItem
{
private static List? s_scheduledTimers;
private static List? s_scheduledTimersToFire;

// TimerQueue 使用了一个 AutoResetEvent 来等待定时器到期,封装了和 OS 定时器的交互
// intialState = false,表示 AutoResetEvent 的初始状态为非终止状态
// 这样,当调用 AutoResetEvent.WaitOne() 时,因为 AutoResetEvent 的状态为非终止状态,那么调用线程会被阻塞
// 被阻塞的线程会在 AutoResetEvent.Set() 被调用时被唤醒
// AutoResetEvent 在被唤醒后,会将自己的状态设置为非终止状态,这样,下一次调用 AutoResetEvent.WaitOne() 时,调用线程会被阻塞
private static readonly AutoResetEvent s_timerEvent = new AutoResetEvent(false);

private bool _isScheduled;
private long _scheduledDueTimeMs;

private bool SetTimer(uint actualDuration)
{
    long dueTimeMs = TickCount64 + (int)actualDuration;
    AutoResetEvent timerEvent = s_timerEvent;
    lock (timerEvent)
    {
        if (!_isScheduled)
        {
            List<TimerQueue> timers = s_scheduledTimers ?? InitializeScheduledTimerManager_Locked();

            timers.Add(this);
            _isScheduled = true;
        }

        _scheduledDueTimeMs = dueTimeMs;
    }

    // 调用 AutoResetEvent.Set(),唤醒 TimerThread
    timerEvent.Set();
    return true;
}

private static List<TimerQueue> InitializeScheduledTimerManager_Locked()
{
    var timers = new List<TimerQueue>(Instances.Length);
    s_scheduledTimersToFire ??= new List<TimerQueue>(Instances.Length);

    Thread timerThread = new Thread(TimerThread)
    {
        Name = ".NET Timer",
        IsBackground = true // 后台线程,当所有前台线程都结束时,后台线程会自动结束
    };
    // 使用 UnsafeStart 方法启动线程,是为了避免 ExecutionContext 的传播
    timerThread.UnsafeStart();

    // 这边是个设计上的细节,如果创建线程失败,那么会在下次创建线程时再次尝试
    s_scheduledTimers = timers;
    return timers;
}

// 这个方法会在一个专用的线程上执行,它的作用是处理定时器请求,并在定时器到期时通知 TimerQueue
private static void TimerThread()
{
    AutoResetEvent timerEvent = s_timerEvent;
    List<TimerQueue> timersToFire = s_scheduledTimersToFire!;
    List<TimerQueue> timers;
    lock (timerEvent)
    {
        timers = s_scheduledTimers!;
    }

    // 初始的Timeout.Infinite表示永不超时,也就是说,一开始只有等到 AutoResetEvent.Set() 被调用时,线程才会被唤醒
    int shortestWaitDurationMs = Timeout.Infinite;
    while (true)
    {
        // 等待定时器到期或者被唤醒
        timerEvent.WaitOne(shortestWaitDurationMs);

        long currentTimeMs = TickCount64;
        shortestWaitDurationMs = int.MaxValue;
        lock (timerEvent)
        {
            // 遍历所有的 TimerQueue,找到到期的 TimerQueue
            for (int i = timers.Count - 1; i >= 0; --i)
            {
                TimerQueue timer = timers[i];
                long waitDurationMs = timer._scheduledDueTimeMs - currentTimeMs;
                if (waitDurationMs <= 0)
                {
                    timer._isScheduled = false;
                    timersToFire.Add(timer);

                    int lastIndex = timers.Count - 1;
                    if (i != lastIndex)
                    {
                        timers[i] = timers[lastIndex];
                    }

                    timers.RemoveAt(lastIndex);
                    continue;
                }
                
                // 找到最短的等待时间
                if (waitDurationMs < shortestWaitDurationMs)
                {
                    shortestWaitDurationMs = (int)waitDurationMs;
                }
            }
        }

        if (timersToFire.Count > 0)
        {
            foreach (TimerQueue timerToFire in timersToFire)
            {
                // 将到期的 TimerQueue 放入到线程池中执行
                // UnsafeQueueHighPriorityWorkItemInternal 方法会将 timerToFire 放入到线程池的高优先级队列中,这个是 .NET 7 中新增的功能
                ThreadPool.UnsafeQueueHighPriorityWorkItemInternal(timerToFire);
            }

            timersToFire.Clear();
        }

        if (shortestWaitDurationMs == int.MaxValue)
        {
            shortestWaitDurationMs = Timeout.Infinite;
        }
    }
}

void IThreadPoolWorkItem.Execute() => FireNextTimers();

}
所有的 TimerQueue 共享一个 AutoResetEvent 和一个 TimerThread,当 AutoResetEvent.Set() 被调用或者OS定时器到期时,TimerThread 会被唤醒,然后 TimerThread 会遍历所有的 TimerQueue,找到到期的 TimerQueue,然后将到期的 TimerQueue 放入到线程池中执行。

这样,就实现了 TimerQueue 的定时器管理器。

总结
TimerQueue 的实现是一个套娃的过程。

TimerQueue 使用了一个 AutoResetEvent 来等待定时器到期,封装了和 OS 定时器的交互,然后 TimerQueue 实现了 IThreadPoolWorkItem 接口,这意味着 TimerQueue 可以被放入到线程池中执行。

TimerQueue 的定时器管理器是一个专用的线程,它会等待 AutoResetEvent.Set() 被调用或者OS定时器到期时被唤醒,然后遍历所有的 TimerQueue,找到到期的 TimerQueue,然后将到期的 TimerQueue 放入到线程池中执行。

TimerQueue 在被放入到线程池中执行时,会调用 FireNextTimers 方法,这个方法会遍历 TimerQueue 保存的 TimerQueueTimer,找到到期的 TimerQueueTimer,然后将到期的 TimerQueueTimer 放入到线程池中执行。

标签:定时器,AutoResetEvent,到期,TimerQueue,线程,NET,揭秘,WaitOne
From: https://www.cnblogs.com/zhangsai/p/17620701.html

相关文章

  • 【Archaius技术专题】「Netflix原生态」动态化配置服务之微服务配置组件变色龙
    推荐超值课程:点击获取前提介绍如果要设计开发一套微服务基础架构,参数化配置是一个非常重要的点,而Netflix也开源了一个叫变色龙Archaius的配置中心客户端,而且Archaius可以说是比其他客户端具备更多生产级特性,也更灵活。*在NetflixOSS微服务技术栈中,几乎所有的其它组件(例如Zuul......
  • .NET JIT脱壳指南与工具源码
    title:.NETJIT脱壳指南与工具源码date:2019-08-08updated:2023-04-09lang:zh-CNcategories:-[.NET逆向]tags:-.NET-逆向工程-脱壳-JITtoc:true文章首发于https://wwh1004.com/net-jit-unpack-guide-and-source/本文介绍了.NET下的JIT层加密点与脱壳技巧......
  • .NET下绕过任意反Dump的方法
    title:.NET下绕过任意反Dump的方法date:2022-03-16updated:2023-04-12lang:zh-CNcategories:-[.NET逆向]tags:-.NET-逆向工程-反转储toc:true文章首发于https://wwh1004.com/net-trick-to-bypass-any-anti-dumping/本文介绍了一种通过CLR内部数据绕过任意......
  • .NET下的终极反调试
    title:.NET下的终极反调试date:2018-12-22updated:2023-04-12lang:zh-CNcategories:-[.NET逆向]tags:-.NET-逆向工程-反调试toc:true文章首发于https://wwh1004.com/net-ultimate-anti-debugging/本文介绍了.NET下的反调试原理,包括CLR内部调试机制。通过本......
  • dotnet restore
    错误NU1105找不到“xx.csproj”的项目信息。如果使用VisualStudio,这可能是因为该项目已被卸载或不属于当前解决方案,因此请从命令行运行还原。否则,项目文件可能无效或缺少还原所需的目标。xxxxxx.csproj 打开Nuget包管理器->程序包管理器控制台 dotnetrestore......
  • 反混淆VMProtect.NET之Mutation
    title:反混淆VMProtect.NET之Mutationdate:2019-08-09updated:2023-04-11lang:zh-CNcategories:-[.NET逆向]tags:-.NET-逆向工程-脱壳-VMProtect-变异toc:true文章首发于https://wwh1004.com/deobfuscating-mutation-of-vmprotect_net/本文介绍了VMPro......
  • c#.net 大文件分片上传处理
    ​ IE的自带下载功能中没有断点续传功能,要实现断点续传功能,需要用到HTTP协议中鲜为人知的几个响应头和请求头。 一. 两个必要响应头Accept-Ranges、ETag        客户端每次提交下载请求时,服务端都要添加这两个响应头,以保证客户端和服务端将此下载识别为可以断点续......
  • ABP.NET创建项目(一)
    ABP.NET创建项目相关文档1(下半部分)相关文档2(MySql部分)1.按照相关文档1的上半部分下载ABP2.需要额外安装的NuGet包3.需要自己建立的文件(Red)&需要更改的原始文件(Green)3.1:需要更改的原始文件(Green)一:MyProjectDbContext.cs:usingMicrosoft.Enti......
  • 用户空间协议栈设计和netmap综合指南
    本文分享自华为云社区《用户空间协议栈设计和netmap综合指南,将网络效率提升到新高度》,作者:LionLong。协议概念1.1、七层网络模型和五层网络模型应用层: 最接近用户的一层,为用户程序提供网络服务。主要协议有HTTP、FTP、TFTP、SMTP、DNS、POP3、DHCP等。表示层: 数据的表示......
  • 聚焦Web前端安全:最新揭秘漏洞防御方法
    在Web安全中,服务端一直扮演着十分重要的角色。然而前端的问题也不容小觑,它也会导致信息泄露等诸如此类的问题。在这篇文章中,我们将向读者介绍如何防范Web前端中的各种漏洞。【万字长文,请先收藏再阅读】首先,我们需要了解安全防御产品已经为我们做了哪些工作。其次,我们将探讨前端......