给出附加的LINQ-Pad片段。
它创建8个任务,执行500毫秒,并在线程实际运行时绘制图表。
现在,如果我在线程循环中添加一个Thread.Sleep 或一个Task.Delay,我可以看到windows系统计时器的时钟(~15ms):
现在,还有timeBeginPeriod
函数,我可以降低系统计时器的分辨率(在本例中为1ms)。这就是区别。使用Thread.Sleep
我得到此图表(我的预期):
使用Task.Delay
时,我会得到与时间设置为15毫秒相同的图表:
问题:为什么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);
}
}
答案 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)。