Sleep well

Published on Sunday, October 29, 2017

How many ways to sleep a thread do you know?

Usually you need to sleep a thread in a loop when you execute some repeatable operation (checking results every 5 seconds, for example). So, you need to stop this loop for a period of time.

Thread.Sleep

The most popular way - use Thread.Sleep method:

private static void Test1()
{
    while (true)
    {
        Console.WriteLine("Checking...");
        if (File.Exists("C:\\1.txt"))
            break;

        Thread.Sleep(TimeSpan.FromSeconds(5));
    }
}

The simplest way, but it has one big defect. Method Sleep is synchronous and cannot be interrupted. So, you need to wait 5 seconds before exit. To solve this problem, you can use some sort of WaitHandle.

WaitHandle

private static void Test2()
{
    var autoResetEvent = new AutoResetEvent(false);

    while (true)
    {
        Console.WriteLine("Checking...");
        if (File.Exists("C:\\1.txt"))
            break;

        if (autoResetEvent.WaitOne(TimeSpan.FromSeconds(5)))
            break;
    }
}

Method WaitOne blocks the current thread until the current instance receives a signal. Argument specifies a period of time to wait the signal. After the period method the thread exit from the method and continue to execute.

EventWaitHandle (base class for AutoResetEvent) has method Set to signal the instance. After receiving this signal AutoResetEvent immediately frees the thread and the code continues to execute without waiting excessive 5 seconds:

autoResetEvent.Set();

WaitOne will return true, it instance get signal.

Task.Delay

Since .net 4, when Microsoft introduces concept of asynchronous operations with Tasks, we get new improved equivalent to Thread.Sleep - Task.Delay.

private static async Task Test3()
{
    var tokenSource = new CancellationTokenSource();

    while (true)
    {
        Console.WriteLine("Checking...");
        if (File.Exists("C:\\1.txt"))
            break;

        await Task.Delay(TimeSpan.FromSeconds(5), tokenSource.Token);

        if (tokenSource.Token.IsCancellationRequested)
            break;
    }
}

Second argument - CancellationToken allows us to control execution of Delay. But unlike WaitHandle where you can set signal several times, CancellationTokenSource can cancel Task only once. Check IsCancellationRequested property to detect if you need to exit. And the loop can be transformed:

private static async Task Test3()
{
    var tokenSource = new CancellationTokenSource();

    while (!tokenSource.Token.IsCancellationRequested)
    {
        Console.WriteLine("Checking...");
        if (File.Exists("C:\\1.txt"))
            break;

        await Task.Delay(TimeSpan.FromSeconds(5), tokenSource.Token);
    }
}

CancellationToken

CancellationToken itself contains WaitHandle and it can be used for delay:

private static void Test4()
{
    var tokenSource = new CancellationTokenSource();

    while (!tokenSource.Token.IsCancellationRequested)
    {
        Console.WriteLine("Checking...");
        if (File.Exists("C:\\1.txt"))
            break;

        tokenSource.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(5));
    }
}

Benchmark

Simple benchmark (with Benchmark.NET) to check delay intervals:

namespace SleepWell
{
    public class Program
    {
        private CancellationToken _token = new CancellationToken();

        static void Main(string[] args)
        {
            BenchmarkRunner.Run<Program>();
        }

        [Benchmark]
        public void Benchmark1()
        {
            Thread.Sleep(TimeSpan.FromMilliseconds(2));
        }

        [Benchmark]
        public async Task Benchmark2()
        {
            await Task.Delay(TimeSpan.FromMilliseconds(2));
        }

        [Benchmark]
        public void Benchmark3()
        {
            _token.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(2));
        }
    }
}

Results:

     Method |      Mean |     Error |    StdDev |
----------- |----------:|----------:|----------:|
 Benchmark1 |  2.348 ms | 0.0218 ms | 0.0193 ms |
 Benchmark2 | 15.614 ms | 0.0199 ms | 0.0186 ms |
 Benchmark3 |  2.235 ms | 0.0153 ms | 0.0143 ms |

Old-school Thread.Sleep and modern CancellationToken can handle very small interval, while Task.Delay has limit to about 15ms. The reason - Task.Delay is build on Timer that has default resolution 15.6 milliseconds. WaitHandle and Thread use native calls to sleep a thread.