Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Thread.Sleep vs. Task.Delay when using timeBeginPeriod / Task scheduling

Given the attached LINQ-Pad snippet.

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

On a 4 core CPU it may look like this: enter image description here

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):

enter image description here

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):

enter image description here

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

enter image description here

Question: Why does the TPL ignore the timer setting?

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);
    }
}
like image 534
Tho Mai Avatar asked Dec 28 '17 12:12

Tho Mai


1 Answers

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.

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:

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.

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.

like image 165
Hans Passant Avatar answered Sep 29 '22 18:09

Hans Passant