使用timeBeginPeriod /任务调度时Thread.Sleep与Task.Delay

时间:2017-12-28 12:31:09

标签: c# multithreading task-parallel-library

给出附加的LINQ-Pad片段。

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

在4核CPU上,它可能如下所示: enter image description here

现在,如果我在线程循环中添加一个Thread.Sleep 一个Task.Delay,我可以看到windows系统计时器的时钟(~15ms):

enter image description here

现在,还有timeBeginPeriod函数,我可以降低系统计时器的分辨率(在本例中为1ms)。这就是区别。使用Thread.Sleep我得到此图表(我的预期):

enter image description here

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

enter image description here

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

这是代码(你需要LinqPad 5.28 beta来运行图表)

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);
    }
}

2 个答案:

答案 0 :(得分:8)

timeBeginPeriod()是一个遗留函数,可以追溯到Windows 3.1。微软希望摆脱它,但不能。它具有相当大的机器范围副作用,它增加了时钟中断率。时钟中断是操作系统的“心跳”,它确定线程调度程序何时运行以及何时可以恢复休眠线程。

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

  

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

最后的警告是微软对此不太满意的原因。这确实被误用了,这个网站的创始人之一在this blog post中注意到了一个更令人震惊的案例。谨防带有礼物的希腊人。

这改变了定时器的准确性并不是一个特征。您不希望程序因为用户启动浏览器而表现不同。所以.NET设计师做了一些事情。 Task.Delay()使用System.Threading.Timer。它不是盲目地依赖中断率,而是将指定的周期除以15.6来计算时间片的数量。稍微偏离理想值btw,即15.625,但是整数数学的副作用。因此,当时钟速率降低到1毫秒时,定时器的行为可预测并且不再行为不当,它总是需要至少一个切片。实际上16毫秒,因为GetTickCount()单位是毫秒。

答案 1 :(得分:0)

Task.Delay() 是通过 TimerQueueTimer (ref DelayPromise) 实现的。后者的分辨率不会根据 timeBeginPeriod() 变化,尽管这似乎是 Windows 中的最新变化(参考 VS Feedback)。