大量的计时器

时间:2013-12-06 15:16:05

标签: c# .net timer

我需要编写一个接收事件的组件(事件具有唯一的ID)。每个活动都要求我发出请求。该事件指定一个超时期限,等待来自请求的响应。

如果响应在计时器触发之前发生,那很好,我取消了计时器。 如果计时器首先触发,那么请求超时,我想继续前进。

此超时期限在事件中指定,因此它不是常量。 预期的超时时间在30秒到5分钟的范围内。

我可以看到实现这两种方法。

  1. 为每个事件创建一个计时器,并将其放入将事件链接到计时器的字典中。
  2. 创建一个包含超时日期时间的有序列表,以及每隔100毫秒循环一次的新线程,以检查是否超时。
  3. 选项1似乎是最简单的解决方案,但我担心创建这么多计时器可能不是一个好主意,因为计时器可能太贵了。在创建大量计时器时是否存在任何陷阱?我怀疑在后台,定时器实现可能实际上是选项2的有效实现。如果这个选项是个好主意,我应该使用哪个定时器? System.Timers.Timer或System.Threading.Timer。

    选项2似乎更有效,与选项1相比可能不是一种有效的解决方案。

    更新

    我期望的最大定时器数量在10000的范围内,但更有可能在100的范围内。另外,正常情况是在触发前取消定时器。

    更新2

    我使用System.Threading.TimerSystem.Timers.Timer的10K实例运行测试,密切关注线程数和内存。 System.Threading.Timer与内存使用情况判断相比System.Timers.Timer似乎“更轻”,并且两个定时器都没有创建过多的线程(即 - 线程池正常工作)。所以我决定继续使用System.Threading.Timer

5 个答案:

答案 0 :(得分:5)

我在嵌入式系统(纯c)中做了很多,我无法刻录大量资源(例如,4k的RAM是系统内存)。这是一种已成功使用的方法:

  1. 创建一个定期关闭的系统定时器(中断)(例如每10 ms)。
  2. “计时器”是动态列表中的一个条目,表示在计时器关闭之前剩余多少“刻度”。
  3. 每次系统计时器熄灭时,迭代列表并递减每个“计时器”。每一个零都被“解雇”。将其从列表中删除,并执行计时器应该执行的任何操作。
  4. 定时器关闭时会发生什么情况取决于应用程序。它可能是状态机运行。它可能是一个被调用的函数。它可能是一个枚举,告诉执行代码如何处理参数,并将其发送给“Create Timer”调用。定时器结构中的信息是设计上下文中的必要信息。 “滴答计数”是秘诀。

    我们还创建了这个返回定时器的“ID”(通常是从池中抽取的定时器结构的地址),因此可以取消它或在其上获得状态。

    便利功能将“秒”转换为“滴答”,因此创建定时器的API始终以“秒”或“毫秒”表示。

    您可以将“tick”间隔设置为合理的粒度权衡值。

    我已经在C ++,C#,objective-C中完成了其他实现,一般方法几乎没有变化。它是一种非常通用的定时器子系统设计/架构。你只需要一些东西来创造基本的“滴答声”。

    我甚至用一个紧密的“主”循环和高精度内部计时器的秒表做了一次,当我没有计时器时创建我自己的“模拟”刻度。我不推荐这种方法;我在一个直接的控制台应用程序中模拟硬件,并且无法访问系统计时器,所以这是一个极端的情况。

    在一台现代处理器上迭代数百个定时器的列表每秒10次并不是什么大不了的事。您可以通过插入带有“delta seconds”的项目并按排序顺序将它们放入列表中来克服此问题。这样您只需检查列表前面的那些。这会让你超越扩展问题,至少在迭代列表方面。

    这有用吗?

答案 1 :(得分:4)

第一个选项根本就不会扩展,如果你有很多并发超时,你将需要做其他的事情。 (如果您不知道自己有多少就足够成为一个问题,请随时尝试使用计时器来确定您是否确实遇到问题。)

那就是说,你的第二个选择需要一些调整。只需创建一个单个计时器,并将其间隔(每次触发)设置为当前时间和“下一个”超时时间之间的时间跨度,而不是在新线程中使用紧密循环。 / p>

答案 2 :(得分:3)

你应该以最简单的方式做到这一点。如果您担心性能,则应通过分析器运行应用程序并确定瓶颈。您可能会非常惊讶地发现它是您最不期望的代码,并且您已经无缘无故地优化了代码。我总是编写最简单的代码,因为这是最简单的。见PrematureOptimization

我不明白为什么会有大量计时器出现任何陷阱。我们在谈论十几个,或100个,还是10,000个?如果它非常高,你可能会遇到问题。您可以编写一个快速测试来验证这一点。

至于哪些Timer类使用:我不想窃取任何可能做更多研究的人的答案:查看this answer to that question`

答案 3 :(得分:1)

让我提出一个不同的架构:对于每个事件,只需创建一个新的Task并发送请求并等待 1 以获得响应。

〜{1000>任务应该可以正常扩展,如this early demo所示。我怀疑〜10000个任务仍然可以扩展,但我自己没有测试过。


1 考虑通过在Task.Delay(而不仅仅是Thread.Sleep)上附加延续来实现等待,以避免订阅不足。

答案 4 :(得分:1)

我认为Task.Delay是一个非常好的选择。这是测试代码,用于测量在不同的延迟时间内可以执行多少个并发任务。该代码还在计算错误统计信息,以提高等待时间的准确性。

    static async Task Wait(int delay, double[] errors, int index)
    {
        var sw = new Stopwatch();
        sw.Start();
        await Task.Delay(delay);
        sw.Stop();
        errors[index] = Math.Abs(sw.ElapsedMilliseconds - delay);
    }

    static void Main(string[] args)
    {
        var trial = 100000;
        var minDelay = 1000;
        var maxDelay = 5000;

        var errors = new double[trial];
        var tasks = new Task[trial];
        var rand = new Random();
        var sw = new Stopwatch();

        sw.Start();
        for (int i = 0; i < trial; i++)
        {
            var delay = rand.Next(minDelay, maxDelay);
            tasks[i] = Wait(delay, errors, i);
        }
        sw.Stop();

        Console.WriteLine($"{trial} tasks started in {sw.ElapsedMilliseconds} milliseconds.");

        Task.WaitAll(tasks);

        Console.WriteLine($"Avg Error: {errors.Average()}");
        Console.WriteLine($"Min Error: {errors.Min()}");
        Console.WriteLine($"Max Error: {errors.Max()}");

        Console.ReadLine();
    }

您可以更改参数以查看不同的结果。以下是几毫秒的结果:

  

100000个任务在9353毫秒内启动。

     

平均错误:9.10898

     

最小错误:0

     

最大错误:110