C# 使用timeBeginPeriod/任务调度时的Thread.Sleep vs.Task.Delay

C# 使用timeBeginPeriod/任务调度时的Thread.Sleep vs.Task.Delay,c#,multithreading,task-parallel-library,C#,Multithreading,Task Parallel Library,给定附加的LINQ Pad片段 它创建8个任务,执行500毫秒,并在线程实际运行时绘制一个图表 在4核CPU上,它可能如下所示: 现在,如果我在线程循环中添加Thread.Sleep或Task.Delay,我可以直观地看到windows系统计时器的时钟(~15ms): 现在,还有timeBeginPeriod函数,在这里我可以降低系统计时器的分辨率(在本例中为1ms)。这就是区别。使用Thread.Sleep我得到了这个图表(我所期望的): 使用Task.Delay时,我得到的图形与将时间

给定附加的LINQ Pad片段

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

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

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

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

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

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

下面是代码(运行图表需要LinqPad 5.28 beta版)

void Main()
{
常量int线程=8;
常量int MaxTask=20;
const int RuntimeMillis=500;
常数int粒度=10;
SetMinThreads(MaxTask,MaxTask);
SetMaxThreads(MaxTask,MaxTask);
var series=newbool[Threads][];
初始化(i=>newbool[RuntimeMillis*粒度];
Watch.Start();
var tasks=Async.tasks(Threads,i=>ThreadFunc(series[i],pool));
任务。等待();
series.ForAll((x,y)=>series[y][x]?新的{x=x/(双)粒度,y=y+1}:null)
.Chart(i=>i.X,i=>i.Y,LINQPad.Util.SeriesType.Point)
.Dump();
异步任务ThreadFunc(bool[]数据,集合点p)
{
现在加倍;
而((现在=Watch.Millis)=max)
waitHandle.Set();
}
}
公共无效等待()
{
waitHandle.WaitOne();
}
公共无效重置()
{
lock(this.lockObject)
{
waitHandle.Reset();
这个。n=0;
}
}
}
公共静态类ArrayExtensions
{
公共静态void Initialize(此T[]数组,Func init)
{
for(int n=0;nStart=Stopwatch.GetTimestamp();
公共静态双毫秒=>(Stopwatch.GetTimestamp()-start)*1000.0/Stopwatch.Frequency;
公共静态void DumpMillis()=>Millis.Dump();
}
公共静态类异步
{
公共静态任务[]任务(int任务,Func线程)
{
返回可枚举范围(0,任务)
.Select(i=>Task.Run(()=>thread(i)))
.ToArray();
}
公共静态void JoinAll(此线程[]个线程)
{
foreach(线程中的var线程)
thread.Join();
}
公共静态无效等待(此任务[]个任务)
{
Task.WaitAll(任务);
}
}
timeBeginPeriod()是一个遗留函数,可追溯到Windows 3.1。微软很想摆脱它,但不能。它有一个相当严重的机器范围的副作用,它增加了时钟中断率。时钟中断是操作系统的“心跳”,它决定线程调度程序何时运行以及休眠线程何时可以恢复

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

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

最后的警告是为什么微软对此不太满意。这确实被误用了,其中一个更可怕的案例被这个网站的创始人之一在。提防带礼物的希腊人

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

Task.Delay()
是通过
TimerQueueTimer
实现的(参考)。后者的分辨率不会根据
timeBeginPeriod()
而改变,尽管这似乎是一个rece
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);
    }
}