The usages and suggestions about Thead.Sleep(0),Task.Delay(0),Thread.Yield(),Task.Yield()

Introduction

There are some methods to release current thread excution right in .NET/C#. They are Thread.Sleep(0),Task.Delay(0),Thread.Yield(),Task.Yeild(). The function of these methods is to tell the program to give up current thread and then run another thread. But there are some differences among these methods.

This article is about their differences and theories.


Detail

The original link is C#/.NET 中 Thread.Sleep(0), Task.Delay(0), Thread.Yield(), Task.Yield() 不同的执行效果和用法建议. But this blog was written in Chinese, so I translate its content to English. Waterlv is an MVP(Microsoft Most Valuable Professional), and he is good at .NET Core\WPF\.NET. Here is his Blog.


Theories

Thread.Sleep(0)

Firstly,I will post the source code of Thread.Spleep(int millisecondsTimeout).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*=========================================================================
** Suspends the current thread for timeout milliseconds. If timeout == 0,
** forces the thread to give up the remainer of its timeslice. If timeout
** == Timeout.Infinite, no timeout will occur.
**
** Exceptions: ArgumentException if timeout < 0.
** ThreadInterruptedException if the thread is interrupted while sleeping.
=========================================================================*/
[System.Security.SecurityCritical] // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern void SleepInternal(int millisecondsTimeout);

[System.Security.SecuritySafeCritical] // auto-generated
public static void Sleep(int millisecondsTimeout)
{
SleepInternal(millisecondsTimeout);
// Ensure we don't return to app code when the pause is underway
if(AppDomainPauseManager.IsPaused)
AppDomainPauseManager.ResumeEvent.WaitOneWithoutFAS();
}

It is easy to find that in the Sleep method it calls another method which’s name is SleepInternal. SleepInternal is implemented in CLR, and its function is suspending the current thread for the value of millisecondsTimeout.

If we set the value of milliseconsTimeout to 0 as Thread.Sleep(0).It will force the current thread to give up the rest of the CPU time slice. Then other threads which have higher priority will run. But if there are not any available threads have higher priority than current thread, this thread will keep running.

If your method will not be affected by other threads, there are not any differences among the methods above. But when your method is affected by multiple threads, other threads may run into this method when you call Thread.Sleep(0).But it is good news that the CPU time slice for a thread is nanosecond level, so this situation is almost impossible to happen.

Thread.Yeild()

Here is the code of Thread.Yeild()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[System.Security.SecurityCritical]  // auto-generated
[ResourceExposure(ResourceScope.None)]
[DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)]
[SuppressUnmanagedCodeSecurity]
[HostProtection(Synchronization = true, ExternalThreading = true),
ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private static extern bool YieldInternal();

[System.Security.SecuritySafeCritical] // auto-generated
[HostProtection(Synchronization = true, ExternalThreading = true),
ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static bool Yield()
{
return YieldInternal();
}

And we can notice that Yield() method calls YieldInternal() method. And YieldInternal is implemented in CLR too.

The function of Thread.Yield() is to force the current thread to give up the rest of CPU time slice, the same as Thread.Sleep(0).

Thread.Sleep(1)

It is just a little differance between Thread.Sleep(1) and Thread.Sleep(0), but their function are different.

The current thread will be suspended for timeout milliseconds(here is 1ms). Therefore, during timeout milliseconds current thread will stay at the un-schedulable state. So, other threads will be run even their priority is lower than the current thread.

Here is the result of these three method’s execution time.

Nothing means there are not any codes. And we used Stopwatch to get the result. For more detail about Stopwatch visit .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间). This blog was written in Chinese, I will translate it later.
Here is a part of the codes.

1
2
3
4
var stopwatch = Stopwatch.StartNew();
Thread.Sleep(0);
var elapsed = stopwatch.Elapsed;
Console.WriteLine($"Thread.Sleep(0) : {elapsed}");

Task.Delay(0)

Task.Delay is a method of TAP. For more informations about TAP,visit Task-based Asynchronous Pattern (TAP) Microsoft Docs.

TAP is a threading model which bases on an async state machine, and this is the biggest difference from the Thread method.

Here is the source code of Task.Delay.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/// <summary>
/// Creates a Task that will complete after a time delay.
/// </summary>
/// <param name="millisecondsDelay">The number of milliseconds to wait before completing the returned Task</param>
/// <param name="cancellationToken">The cancellation token that will be checked prior to completing the returned Task</param>
/// <returns>A Task that represents the time delay</returns>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// The <paramref name="millisecondsDelay"/> is less than -1.
/// </exception>
/// <exception cref="T:System.ObjectDisposedException">
/// The provided <paramref name="cancellationToken"/> has already been disposed.
/// </exception>
/// <remarks>
/// If the cancellation token is signaled before the specified time delay, then the Task is completed in
/// Canceled state. Otherwise, the Task is completed in RanToCompletion state once the specified time
/// delay has expired.
/// </remarks>
public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken)
{
// Throw on non-sensical time
if (millisecondsDelay < -1)
{
throw new ArgumentOutOfRangeException("millisecondsDelay", Environment.GetResourceString("Task_Delay_InvalidMillisecondsDelay"));
}
Contract.EndContractBlock();

// some short-cuts in case quick completion is in order
if (cancellationToken.IsCancellationRequested)
{
// return a Task created as already-Canceled
return Task.FromCancellation(cancellationToken);
}
else if (millisecondsDelay == 0)
{
// return a Task created as already-RanToCompletion
return Task.CompletedTask;
}

// Construct a promise-style Task to encapsulate our return value
var promise = new DelayPromise(cancellationToken);

// Register our cancellation token, if necessary.
if (cancellationToken.CanBeCanceled)
{
promise.Registration = cancellationToken.InternalRegisterWithoutEC(state => ((DelayPromise)state).Complete(), promise);
}

// ... and create our timer and make sure that it stays rooted.
if (millisecondsDelay != Timeout.Infinite) // no need to create the timer if it's an infinite timeout
{
promise.Timer = new Timer(state => ((DelayPromise)state).Complete(), promise, millisecondsDelay, Timeout.Infinite);
promise.Timer.KeepRootedWhileScheduled();
}

// Return the timer proxy task
return promise;
}

This method will return Task.CompleteTask as a result when the value of the parameter is 0. That means the code you write after Task.Delay(0) will be executed immediately. (If the rest of CPU time slice is enough).

Task.Yield()

The result of Task.Yield() is a YeildAwaitable instance and the YieldAwaitable.GetAwaiter method returns an instance of YieldAwaiter. So, the result of Task.Yield method is fully depending on YieldAwaiter. For more details about Awaiter visit 如何实现一个可以用 await 异步等待的 Awaiter.

YieldAwaiter relies on QueueContinuation to determine when to execute subsequent codes. Part of the source codes is listed below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Get the current SynchronizationContext, and if there is one,
// post the continuation to it. However, treat the base type
// as if there wasn't a SynchronizationContext, since that's what it
// logically represents.
var syncCtx = SynchronizationContext.CurrentNoFlow;
if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
{
syncCtx.Post(s_sendOrPostCallbackRunAction, continuation);
}
else
{
// If we're targeting the default scheduler, queue to the thread pool, so that we go into the global
// queue. As we're going into the global queue, we might as well use QUWI, which for the global queue is
// just a tad faster than task, due to a smaller object getting allocated and less work on the execution path.
TaskScheduler scheduler = TaskScheduler.Current;
if (scheduler == TaskScheduler.Default)
{
if (flowContext)
{
ThreadPool.QueueUserWorkItem(s_waitCallbackRunAction, continuation);
}
else
{
ThreadPool.UnsafeQueueUserWorkItem(s_waitCallbackRunAction, continuation);
}
}
// We're targeting a custom scheduler, so queue a task.
else
{
Task.Factory.StartNew(continuation, default(CancellationToken), TaskCreationOptions.PreferFairness, scheduler);
}
}

There are two branches in these codes.

  1. One is setting DispatcherSynchronizationContext as the value of SynchronizationContext , then it will call Post method in SynchronizationContext to execute next asynchronous task. The ‘continuation’ means executes the next asynchronous task.
    The value of SynchronizationContext is set as DispatcherSynchronizationContext for WPF UI thread, and its Post method is designed to implement the message loop. If other threads have no special setting, the value of SynchronizationContext is always null. For more information,see 出让执行权:Task.Yield, Dispatcher.Yield.

  2. If the value of DispacherSynchronizationContext is null or SynchronizationContext type, the codes above will run else logic. And its logic is depending on the value of TaskScheduler.Current, it will find next thread in thread pool or start a Task again.

Task.Delay(1)

Task.Delay(1) is almost the same as Task.Delay(0), but the function of these two codes is different.

Task.Delay(1) starts a System.Threading.Timer instance, and it will describe a callback method which is executed when time is up.

Here is the exact API order to excute the callback

Timer.TimerSetup->TimerHolder->TimerQueueTimer->TimerQueue.UpdateTimer->EnsureAppDomainTimerFiresBy->ChangeAppDomainTimer->callback

The codes after await will be encapsulated by the asynchronous state machine and passed to the callback above.

Here is the code of ChangeAppDomainTimer method.

1
2
3
4
5
[System.Security.SecurityCritical]
[ResourceExposure(ResourceScope.None)]
[DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)]
[SuppressUnmanagedCodeSecurity]
static extern bool ChangeAppDomainTimer(AppDomainTimerSafeHandle handle, uint dueTime);

And when we call the methods in Thread, they just affect the scheduling status of the current thread. But when we call the methods in Task, they will affect the scheduling of the thread pool, then they will call System.Threading.Timer to count time, the time they consume is more uncontrollable.

Here is the result of the time they consumed.

Nothing means there are not any codes.

Differences

The function of Thread.Sleep(0) and Thread.Yield() is the same in thread scheduling. And Thread.Sleep(int) will wait for time out and it bases on thread scheduling.

Thread.Sleep(0) and Thread.Yield() can be called for giving up current thread’s rest time slice, and give chance to run other threads. If you want to make your thread wait, you can call Thread.Sleep(int).

If you have to use async/await , you should use Task.Delay(0) or Task.Yield(). If you use Task.Delay instead of Thread.Sleep, it can save resources for one thread.

References