使用 timeBeginPeriod/任务调度时的 Thread.Sleep 与 Task.Delay [英] Thread.Sleep vs. Task.Delay when using timeBeginPeriod / Task scheduling

查看:53
本文介绍了使用 timeBeginPeriod/任务调度时的 Thread.Sleep 与 Task.Delay的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

给定附加的 LINQ-Pad 片段.

Given the attached LINQ-Pad snippet.

它创建了 8 个任务,执行了 500 毫秒,并绘制了线程实际运行时间的图表.

It creates 8 tasks, executes for 500ms and draws a graph on when the threads were actually running.

在 4 核 CPU 上,它可能如下所示:

On a 4 core CPU it may look like this:

现在,如果我在线程循环中添加一个 Thread.Sleep Task.Delay,我可以可视化 Windows 系统计时器的时钟(~15 毫秒):

Now, if I add a Thread.Sleep or a Task.Delay within the thread loops, I can visualize the clock of the windows system timer (~15ms):

现在,还有 timeBeginPeriod 函数,我可以在其中降低系统计时器的分辨率(在示例中为 1 毫秒).这就是不同之处.使用 Thread.Sleep 我得到这个图表(我所期望的):

Now, there's also the timeBeginPeriod function, where I can lower the system timer's resolution (in the example to 1ms). And here's the difference. With Thread.Sleep I get this chart (what I expected):

当使用 Task.Delay 时,我得到与时间设置为 15ms 时相同的图表:

When using Task.Delay I get the same graph as when the time would be set to 15ms:

问题:为什么 TPL 会忽略计时器设置?

Question: Why does the TPL ignore the timer setting?

这是代码(您需要 LinqPad 5.28 beta 才能运行图表)

Here is the code (you need LinqPad 5.28 beta to run the Chart)

void Main()
{
    const int Threads = 8;
    const int MaxTask = 20;
    const int RuntimeMillis = 500;
    const int Granularity = 10;

    ThreadPool.SetMinThreads(MaxTask, MaxTask);
    ThreadPool.SetMaxThreads(MaxTask, MaxTask);

    var series = new bool[Threads][];
    series.Initialize(i => new bool[RuntimeMillis * Granularity]);

    Watch.Start();
    var tasks = Async.Tasks(Threads, i => ThreadFunc(series[i], pool));
    tasks.Wait();

    series.ForAll((x, y) => series[y][x] ? new { X = x / (double)Granularity, Y = y + 1 } : null)
        .Chart(i => i.X, i => i.Y, LINQPad.Util.SeriesType.Point)
        .Dump();

    async Task ThreadFunc(bool[] data, Rendezvous p)
    {
        double now;
        while ((now = Watch.Millis) < RuntimeMillis)
        {
            await Task.Delay(10);

            data[(int)(now * Granularity)] = true;
        }
    }
}

[DllImport("winmm.dll")] internal static extern uint timeBeginPeriod(uint period);

[DllImport("winmm.dll")] internal static extern uint timeEndPeriod(uint period);

public class Rendezvous
{
    private readonly object lockObject = new object();
    private readonly int max;
    private int n = 0;

    private readonly ManualResetEvent waitHandle = new ManualResetEvent(false);

    public Rendezvous(int count)
    {
        this.max = count;
    }

    public void Join()
    {
        lock (this.lockObject)
        {
            if (++this.n >= max)
                waitHandle.Set();
        }
    }

    public void Wait()
    {
        waitHandle.WaitOne();
    }

    public void Reset()
    {
        lock (this.lockObject)
        {
            waitHandle.Reset();
            this.n = 0;
        }
    }
}

public static class ArrayExtensions
{
    public static void Initialize<T>(this T[] array, Func<int, T> init)
    {
        for (int n = 0; n < array.Length; n++)
            array[n] = init(n);
    }

    public static IEnumerable<TReturn> ForAll<TValue, TReturn>(this TValue[][] array, Func<int, int, TReturn> func)
    {
        for (int y = 0; y < array.Length; y++)
        {
            for (int x = 0; x < array[y].Length; x++)
            {
                var result = func(x, y);
                if (result != null)
                    yield return result;
            }
        }
    }
}

public static class Watch
{
    private static long start;
    public static void Start() => start = Stopwatch.GetTimestamp();
    public static double Millis => (Stopwatch.GetTimestamp() - start) * 1000.0 / Stopwatch.Frequency;
    public static void DumpMillis() => Millis.Dump();
}

public static class Async
{
    public static Task[] Tasks(int tasks, Func<int, Task> thread)
    {
        return Enumerable.Range(0, tasks)
            .Select(i => Task.Run(() => thread(i)))
            .ToArray();
    }

    public static void JoinAll(this Thread[] threads)
    {
        foreach (var thread in threads)
            thread.Join();
    }

    public static void Wait(this Task[] tasks)
    {
        Task.WaitAll(tasks);
    }
}

推荐答案

timeBeginPeriod() 是一个遗留函数,可以追溯到 Windows 3.1.微软很想摆脱它,但不能.它有一个非常严重的机器范围的副作用,它增加了时钟中断率.时钟中断是操作系统的心跳",它决定了线程调度程序何时运行以及休眠线程何时可以恢复.

timeBeginPeriod() is a legacy function, dates back to Windows 3.1. Microsoft would love to get rid of it, but can't. It has a pretty gross machine-wide side-effect, it increases the clock interrupt rate. The clock interrupt is the "heart-beat" of the OS, it determines when the thread scheduler runs and when sleeping threads can be revived.

.NET Thread.Sleep() 函数实际上不是由 CLR 实现的,而是将作业传递给主机.您用于运行此测试的任何内容都只需将作业委托给 Sleep() winapi 函数.哪个受时钟中断率的影响,如 MSDN 文章中所述:

The .NET Thread.Sleep() function is not actually implemented by the CLR, it passes the job to the host. Any you'd use to run this test simply delegates the job to the Sleep() winapi function. Which is affected by the clock interrupt rate, as documented in the MSDN article:

要提高睡眠间隔的准确性,请调用 timeGetDevCaps 函数来确定支持的最小计时器分辨率,并调用 timeBeginPeriod 函数将计时器分辨率设置为其最小值.调用 timeBeginPeriod 时要小心,因为频繁调用会显着影响系统时钟、系统电源使用和调度程序.

To increase the accuracy of the sleep interval, call the timeGetDevCaps function to determine the supported minimum timer resolution and the timeBeginPeriod function to set the timer resolution to its minimum. Use caution when calling timeBeginPeriod, as frequent calls can significantly affect the system clock, system power usage, and the scheduler.

最后的警告是微软对此不太满意的原因.这确实被滥用了,该网站的一位创始人在 这篇博文.提防希腊人携带礼物.

The caution at the end is why Microsoft isn't very happy about it. This does get misused, one of the more egregious cases was noted by one of the founders of this web site in this blog post. Beware of Greeks bearing gifts.

这改变了计时器的准确性并不是一个特性.您不希望您的程序仅仅因为用户启动浏览器而表现不同.所以 .NET 设计者为此做了一些事情.Task.Delay() 在幕后使用 System.Threading.Timer.它不是盲目地依赖中断率,而是将您指定的周期除以 15.6 来计算时间片数.顺便说一句,与理想值 15.625 略有不同,但这是整数数学的副作用.因此,当时钟速率降低到 1 毫秒时,定时器的行为是可预测的,不会再出现错误行为,它总是至少需要一个切片.由于 GetTickCount() 单位为毫秒,因此实际需要 16 毫秒.

That this changes the accuracy of timers is not exactly a feature. You would not want your program to behave differently just because the user started a browser. So the .NET designers did something about it. Task.Delay() uses System.Threading.Timer under the hood. Instead of just blindly relying on the interrupt rate, it divides the period you specify by 15.6 to calculate the number of time slices. Slightly off from the ideal value btw, which is 15.625, but a side-effect of integer math. So the timer behaves predictably and no longer misbehaves when the clock rate is lowered to 1 msec, it always takes at least one slice. 16 msec in practice since the GetTickCount() unit is milliseconds.

这篇关于使用 timeBeginPeriod/任务调度时的 Thread.Sleep 与 Task.Delay的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆