4

I have a WPF project, and in some of the async event handlers in the WMain.xaml.cs I am using the Task.Delay method to suspend the execution of the handler. Example:

private async void Window_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter)
    {
        // Some UI code
        await Task.Delay(1000);
        // Some more UI code
    }
}

I wonder if there is any advantage to replacing the Task.Delay with a custom DispatcherTimer-based delay. For example¹:

static Task MyDelay(int millisecondsDelay)
{
    TaskCompletionSource tcs = new();
    DispatcherTimer timer = new();
    timer.Interval = TimeSpan.FromMilliseconds(millisecondsDelay);
    timer.Tick += (s, e) => { timer.Stop(); tcs.SetResult(); };
    timer.Start();
    return tcs.Task;
}

... and use it in my handler like this:

// Some UI code
await MyDelay(1000);
// Some more UI code

Do I get any benefit from using the MyDelay instead of the Task.Delay, like consuming less resources or being more responsive under load? Or eventually both approaches amount to the same thing?

More specifically I know that the Task.Delay under the hood uses the ThreadPool and the System.Threading timer infrastructure. Does the DispatcherTimer uses the same infrastructure, or it's based on some other WPF-specific device?

Clarification: The code before and after the await Delay interacts with UI components, so it must run on the UI thread.

¹ The example is simplified. In my actual WPF project the MyDelay is an extension method on the DispatcherObject class (from which the Window and UserControl are derived), to minimize the risk of being called on a non-UI thread and not working (gist).

11
  • 1
    Your MyDelay method won't work if it is called on non-UI thread. Some people may think it is error prone. Commented Nov 1 at 17:51
  • FWIW here’s the code for Task.Delay(): github.com/dotnet/runtime/blob/… And here’s DispatcherTimer: github.com/dotnet/wpf/blob/… Delay() seems to use an internal timer pool that works on cross-platform system ticks, and DispatcherTimer specifically calls a user32.dll Windows API. Commented Nov 1 at 23:44
  • @emoacht I've posted the MyDelay simplified as a minimal example. In my actual WPF project it is an extension method on the DispatcherObject class, so it can be called only inside derived classes like Window and UserControl: await this.Delay(1000);. So their is no risk of calling it on a non-UI thread and not working. The DispatcherObject.Dispatcher is passed to the constructor of the DispatcherTimer. Commented Nov 2 at 0:55
  • 1
    @PanagiotisKanavos the DispatcherTimer is a WPF component, not WinForms. It's not disposable, so how am I supposed to dispose it? If you think that my use of the DispatcherTimer creates a leak, you have a strong case for posting an answer. Leaking resources is a pretty significant disadvantage, and .NET developers like myself should be informed about it. As for the actual question, it's the question itself. Believe it or not my question is actual. It's not an illusion or a fiction. I just want to know, mostly out of curiosity, if one approach has any advantage over the other. Commented Nov 3 at 12:20
  • 1
    I know this isn't an answer, but if you want the most precise and resource-lite timer in Windows, nothing beats multimedia timers, which neither DispatcherTimer nor Task.Delay utilize, but which utilize the audio device clock, and don't appear to rely on threading at all. Obviously, though, no matter what you do, the overhead of returning to the UI thread after is almost certainly going to dwarf any performance gain. Commented Nov 3 at 15:28

4 Answers 4

3

This is an experimental, partial answer to my question. I tested whether the custom MyDelay is affected by a saturated ThreadPool. I already knew that the Task.Delay would be affected, by delaying the completion of the Task until the ThreadPool has a thread available to complete it. So I created a new .NET 9 WPF project and put the code below in the Window_Loaded event handler:

private async void Window_Loaded(object sender, RoutedEventArgs e)
{
    // Saturate the ThreadPool
    for (int i = 0; i < Environment.ProcessorCount; i++)
        ThreadPool.QueueUserWorkItem(_ => Thread.Sleep(3000));

    long t0 = Stopwatch.GetTimestamp();
    await Task.Delay(1000);
    //await MyDelay(1000);
    TimeSpan elapsed = Stopwatch.GetElapsedTime(t0);

    MessageBox.Show(@$"ProcessorCount: {Environment.ProcessorCount}
ThreadCount: {ThreadPool.ThreadCount}
Elapsed: {elapsed.TotalMilliseconds:#,0} msec",
"Task.Delay versus custom DispatcherTimer-based delay in WPF");
}

I got this message:

Screenshot 1 (Elapsed 1,521 msec)

So the Task.Delay lasted 500 msec more than requested, because apparently that's how long it takes for a saturated ThreadPool to inject a new thread in this environment (in .NET 9 console applications I know experimentally that it's 1 sec).

Now let's switch from Task.Delay(1000) to MyDelay(1000). Will it make any difference?

Screenshot 2 (Elapsed 1,006 msec)

It does! Apparently the DispatcherTimer doesn't depend on the ThreadPool, and it's not affected when the ThreadPool is saturated. As long as the UI thread is not busy, the DispatcherTimer will tick in time.

I consider this a partial answer, because the DispatcherTimer may have other disadvantages, or even more advantages, that I am not aware of.

Sign up to request clarification or add additional context in comments.

3 Comments

the DispatcherTimer doesn't depend on the ThreadPool this is explained in the DispatcherTimer docs, specifically the remarks - it works on the UI thread. It's also mentioned in the similar question DispatcherTimer vs a regular Timer in WPF app for a task scheduler. Is the actual question how to throttle keyboard events?
The docs only imply this, but if you look at the source you'll find that eventually this is a Win32 timer
@PanagiotisKanavos "Is the actual question how to throttle keyboard events?" -- No, it's not. My question is actual as is. It's not fictitious. I want to suspend the execution of a WPF event handler for an undisclosed reason. The exact reason is irrelevant. There is UI-related code (code that manipulates UI components) before and after the await Task.Delay(1000);. If you want further clarifications about the question, you could post a comment under the question.
1

As promised, I'm posting a second answer with an analysis of the dispatcher queue.

This analysis demonstrates a disadvantage of the DispatcherTimerDelay, which is that two distinct Dispatcher operations are posted to the Dispatcher each time the async event handler is suspended. In comparison the Task.Delay initiates only one Dispatcher operation. The demonstration uses the Dispatcher.Hooks property, and subscribes to the OperationPosted event.

First, let's add debug output when checkpoints are reached and when operations are added to the dispatcher queue.

In the OnDispatcherTimerDelay method, subscribe to the event of adding an operation to the queue and output before and after the await:

        private async void OnDispatcherTimerDelay(object sender, RoutedEventArgs e)
        {
            if (int.TryParse(tbSleepInterval.Text, out int sleepInterval) &&
                int.TryParse(tbDispatcherInvokeCount.Text, out int dispatcherInvokeCount))
            {
                Dispatcher.Hooks.OperationPosted += OnDispatcherOperationPosted;

                Debug.WriteLine("Start await DispatcherTimerDelay");

                double interval = await IntervalTest.DispatcherTimerDelay(1000, sleepInterval, dispatcherInvokeCount);

                Debug.WriteLine("After await DispatcherTimerDelay");

                tbDispatcherTimerDelay.Text = $"{interval:0}";

                Dispatcher.Hooks.OperationPosted -= OnDispatcherOperationPosted;
            }
        }

In the IntervalTest.DispatcherTimerDelay method, we add only the output when passing the control points:

        public static async Task<double> DispatcherTimerDelay(int millisecondsDelay, int sleepInterval = 3000, int dispatcherInvokeCount = 10)
        {
            Debug.WriteLine("Begin DispatcherTimerDelay");

            long t0 = Stopwatch.GetTimestamp();

            TaskCompletionSource tcs = new();
            DispatcherTimer timer = new(DispatcherPriority.Send);
            timer.Interval = TimeSpan.FromMilliseconds(millisecondsDelay);
            timer.Tick += (s, e) => { timer.Stop(); tcs.SetResult(); };
            timer.Start();

            Debug.WriteLine("Call SaturatingThread");

            SaturatingThread(sleepInterval, dispatcherInvokeCount);

            Debug.WriteLine("End SaturatingThread and Start await");
            await tcs.Task;

            TimeSpan elapsed = Stopwatch.GetElapsedTime(t0);
            Debug.WriteLine("End DispatcherTimerDelay");
            return elapsed.TotalMilliseconds;
        }

Launch the application, wait a bit until the "Output" window stops adding data, and then clear the window. The initial value in the "Dispatcher Invoke Count:" field is set to 10, so leave it at that. Click the DispatcherTimerDelay button. Then change the value to 5 and click again.

enter image description here

Looking at the output window, we see, respectively, 10 or 5 additions of operations for the loop in the SaturatingThread method. All other operations are related to starting the timer, raising an event in it, waiting for the task to complete, and adding an operation to execute code after the await.

enter image description here

3 Comments

Very nice EldHasp! You proved your point that awaiting the DispatcherTimer involves two separate Dispatcher operations, which is definitely an advantage for the Task.Delay that involves only one operation. I had no idea about the Dispatcher.Hooks property, good stuff. Would you like me to edit your answer and add some context at the top, so that the readers understand what is the point that this answer tries to prove?
Yes, please edit.
Also pay attention to the very first operation, which is up to 5/10 of the test boot sequences. This operation creates a timer. If the dispatcher is loaded at this point, the addition and start of the timer may also be delayed.
1

You're running incorrect tests. It's clear that Task.Delay(1000) uses the thread pool for the callback, and if all processor threads are busy, it will wait for a thread to become available.
DispatcherTimer runs on the main application thread. This is often called the UI thread, and for good reason. If the UI thread is busy, your implementation of the MyDelay method will also not provide an accurate measured interval.

If you really need a more or less accurate interval value, you need to implement the delay on its own independent thread, whose execution won't be affected by either the thread pool or the UI thread.

The second problem is measuring time using Stopwatch. In this case, calls to the Stopwatch.GetTimestamp() method occur on the UI thread. Therefore, to transfer control from the thread pool to the UI thread after running on it, the UI thread must also be free. Otherwise, there will be an additional delay.

Here is an example of implementing a more precise delay and also provided testing code that allows you to set different loads on the thread pool and UI thread.

using System.Diagnostics;

namespace SOQuestions2025.Questions.TheodorZoulias.question79806454
{
    public class HighPrecisionTimer : IDisposable
    {
        private readonly Thread _timerThread;
        private readonly CancellationTokenSource _cts;
        private readonly AutoResetEvent _signal = new AutoResetEvent(false);
        private readonly PriorityQueue<TaskCompletionSource, long> _queue;
        private readonly object _lock = new object();
        private long _nextTriggerTime = long.MaxValue;

        public HighPrecisionTimer()
        {
            _queue = new PriorityQueue<TaskCompletionSource, long>();
            _cts = new CancellationTokenSource();

            _timerThread = new Thread(() =>
            {
                try
                {
                    Console.WriteLine("HighPrecisionTimer thread started");
                    TimerLoop(_cts.Token);
                    Console.WriteLine("HighPrecisionTimer thread finished");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"HighPrecisionTimer thread crashed: {ex}");
                }
            })
            {
                IsBackground = true,
                Name = "HighPrecisionTimerThread",
                Priority = ThreadPriority.Highest
            };
            _timerThread.Start();
        }

        public Task Delay(int milliseconds)
        {
            var tcs = new TaskCompletionSource();
            var triggerTime = Stopwatch.GetTimestamp() +
                             (long)(milliseconds * Stopwatch.Frequency / 1000.0);

            lock (_lock)
            {
                _queue.Enqueue(tcs, triggerTime);

                if (triggerTime < _nextTriggerTime)
                {
                    _nextTriggerTime = triggerTime;
                    _signal.Set();
                }
            }

            return tcs.Task;
        }

        private void TimerLoop(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                try
                {
                    long now = Stopwatch.GetTimestamp();
                    bool hasWork = false;

                    while (true)
                    {
                        TaskCompletionSource tcs = null;
                        long nextTime = long.MaxValue;

                        lock (_lock)
                        {
                            if (_queue.Count > 0 && _queue.TryPeek(out var nextTcs, out var nextTrigger) && nextTrigger <= now)
                            {
                                _queue.TryDequeue(out tcs, out _);
                            }

                            if (_queue.Count > 0 && _queue.TryPeek(out _, out nextTime))
                            {
                                _nextTriggerTime = nextTime;
                            }
                            else
                            {
                                _nextTriggerTime = long.MaxValue;
                            }
                        }

                        if (tcs != null)
                        {
                            tcs.TrySetResult();
                            hasWork = true;
                        }
                        else
                        {
                            break;
                        }
                    }

                    int waitTime;
                    lock (_lock)
                    {
                        if (_queue.Count == 0)
                        {
                            waitTime = Timeout.Infinite;
                        }
                        else
                        {
                            long nextTrigger = _nextTriggerTime;
                            long timeToWait = (nextTrigger - now) * 1000 / Stopwatch.Frequency;
                            waitTime = Math.Max(1, Math.Min((int)timeToWait, 50));
                        }
                    }

                    if (hasWork)
                    {
                        Thread.Sleep(1);
                    }
                    else if (waitTime == Timeout.Infinite)
                    {
                        _signal.WaitOne();
                    }
                    else
                    {
                        if (waitTime > 10)
                        {
                            _signal.WaitOne(waitTime);
                        }
                        else
                        {
                            var targetTime = Stopwatch.GetTimestamp() +
                                           (long)(waitTime * Stopwatch.Frequency / 1000.0);
                            while (Stopwatch.GetTimestamp() < targetTime &&
                                   !cancellationToken.IsCancellationRequested)
                            {
                                Thread.SpinWait(100);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"TimerLoop error: {ex}");
                    Thread.Sleep(10);
                }
            }
        }

        public void Dispose()
        {
            _cts.Cancel();
            _signal.Set();

            lock (_lock)
            {
                while (_queue.Count > 0)
                {
                    if (_queue.TryDequeue(out var tcs, out _))
                    {
                        tcs.TrySetCanceled();
                    }
                }
            }

            _signal.Dispose();
            _cts.Dispose();
        }
    }
}
using System.Diagnostics;
using System.Windows;
using System.Windows.Threading;

namespace SOQuestions2025.Questions.TheodorZoulias.question79806454
{
    public static class IntervalTest
    {
        public static void SaturatingThread(int sleepInterval, int dispatcherInvokeCount)
        {
            for (int i = 0; i < 4*Environment.ProcessorCount; i++)
                ThreadPool.QueueUserWorkItem(_ => Thread.Sleep(sleepInterval));
            for (int i = 0; i < dispatcherInvokeCount; i++)
            {
                Application.Current.Dispatcher.BeginInvoke(() => { Thread.Sleep(sleepInterval / 10); }, DispatcherPriority.Normal);
            }
        }

        public static async Task<double> TaskDelay(int millisecondsDelay, int sleepInterval = 3000, int dispatcherInvokeCount = 10)
        {
            SaturatingThread(sleepInterval, dispatcherInvokeCount);

            long t0 = Stopwatch.GetTimestamp();
            await Task.Delay(millisecondsDelay);

            TimeSpan elapsed = Stopwatch.GetElapsedTime(t0);
            return elapsed.TotalMilliseconds;
        }

        public static async Task<double> DispatcherTimerDelay(int millisecondsDelay, int sleepInterval = 3000, int dispatcherInvokeCount = 10)
        {
            long t0 = Stopwatch.GetTimestamp();

            TaskCompletionSource tcs = new();
            DispatcherTimer timer = new();
            timer.Interval = TimeSpan.FromMilliseconds(millisecondsDelay);
            timer.Tick += (s, e) => { timer.Stop(); tcs.SetResult(); };
            timer.Start();
            SaturatingThread(sleepInterval, dispatcherInvokeCount);
            await tcs.Task;

            TimeSpan elapsed = Stopwatch.GetElapsedTime(t0);
            return elapsed.TotalMilliseconds;
        }


        private static readonly HighPrecisionTimer timer = new HighPrecisionTimer();
        public static async Task<double> CustomDelay(int millisecondsDelay, int sleepInterval = 3000, int dispatcherInvokeCount = 10)
        {
            SaturatingThread(sleepInterval, dispatcherInvokeCount);

            long t0 = Stopwatch.GetTimestamp();
            await timer.Delay(millisecondsDelay);

            TimeSpan elapsed = Stopwatch.GetElapsedTime(t0);
            return elapsed.TotalMilliseconds;
        }

    }
}
<Window x:Class="SOQuestions2025.Questions.TheodorZoulias.question79806454.DelayTestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SOQuestions2025.Questions.TheodorZoulias.question79806454"
        mc:Ignorable="d"
        Title="DelayTestWindow" Height="450" Width="800">
    <UniformGrid Columns="2">

        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBlock Text="Sleep Interval: "/>
            <TextBox x:Name="tbSleepInterval" Text="3000" MinWidth="100"/>
        </StackPanel>
        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBlock Text="Dispatcher Invoke Count: "/>
            <TextBox x:Name="tbDispatcherInvokeCount" Text="10" MinWidth="100"/>
        </StackPanel>

        <Button Content="Task Delay"
                Margin="10" Padding="15 5" VerticalAlignment="Center" HorizontalAlignment="Center"
                Click="OnTaskDelay"/>
        <TextBox x:Name="tbTaskDelay"
                 VerticalAlignment="Center" HorizontalAlignment="Center"
                 FontSize="30" Background="SkyBlue"
                 IsReadOnly="True" MinWidth="100"/>

        <Button Content="DispatcherTimer Delay"
                Margin="10" Padding="15 5" VerticalAlignment="Center" HorizontalAlignment="Center"
                Click="OnDispatcherTimerDelay"/>
        <TextBox x:Name="tbDispatcherTimerDelay"
                 VerticalAlignment="Center" HorizontalAlignment="Center"
                 FontSize="30" Background="SkyBlue"
                 IsReadOnly="True" MinWidth="100"/>

        <Button Content="Custom Delay"
                Margin="10" Padding="15 5" VerticalAlignment="Center" HorizontalAlignment="Center"
                Click="OnCustomDelay"/>
        <TextBox x:Name="tbCustomDelay"
                 VerticalAlignment="Center" HorizontalAlignment="Center"
                 FontSize="30" Background="SkyBlue"
                 IsReadOnly="True" MinWidth="100"/>

    </UniformGrid>
</Window>
using System.Windows;

namespace SOQuestions2025.Questions.TheodorZoulias.question79806454
{
    public partial class DelayTestWindow : Window
    {
        public DelayTestWindow()
        {
            InitializeComponent();
        }

        private async void OnTaskDelay(object sender, RoutedEventArgs e)
        {
            if (int.TryParse(tbSleepInterval.Text, out int sleepInterval) &&
                int.TryParse(tbDispatcherInvokeCount.Text, out int dispatcherInvokeCount))
            {
                double interval = await IntervalTest.TaskDelay(1000, sleepInterval, dispatcherInvokeCount);
                tbTaskDelay.Text = $"{interval:0}";
            }
        }

        private async void OnDispatcherTimerDelay(object sender, RoutedEventArgs e)
        {
            if (int.TryParse(tbSleepInterval.Text, out int sleepInterval) &&
                int.TryParse(tbDispatcherInvokeCount.Text, out int dispatcherInvokeCount))
            {
                double interval = await IntervalTest.DispatcherTimerDelay(1000, sleepInterval, dispatcherInvokeCount);
                tbDispatcherTimerDelay.Text = $"{interval:0}";
            }
        }

        private async void OnCustomDelay(object sender, RoutedEventArgs e)
        {
            if (int.TryParse(tbSleepInterval.Text, out int sleepInterval) &&
                int.TryParse(tbDispatcherInvokeCount.Text, out int dispatcherInvokeCount))
            {
                double interval = await IntervalTest.CustomDelay(1000, sleepInterval, dispatcherInvokeCount);
                tbCustomDelay.Text = $"{interval:0}";
            }
        }
    }
}

Cold test - immediately after start, the Task Delay method shows a longer time:
enter image description here

Warm test - the following tests, except for the first one, show times between 1500 and 2100 ms. DispatcherTimer Delay shows 3000-3200 ms, Custom Delay - 1200-1300 ms:
enter image description here

If you disable UI thread loading, Custom Delay shows very accurate values ​​of 1000-1002ms. DispatcherTimer Delay shows times up to 1018ms. TaskDelay ranges from 1200 to 2100ms. TaskDelay is sensitive to the time between clicks. If you don't call methods for half a minute or more, the time is 2100ms or slightly more. If you don't set a timeout between calls, the time is slightly more than 1200ms:
enter image description here

17 Comments

Thanks EldHasp for the answer. Obviously you put a lot of work in it. Could you also share the results of your experiment? I am not seeing them inside the answer.
I added the results to my answer
"If the UI thread is busy, your implementation of the MyDelay method will also not provide an accurate measured interval." -- Sure, if the UI thread is busy then any Delay implementation will fail to resume my async event handler after the desirable interval, because the continuation after the await has to run on the UI thread. I haven't specified in the question that my event handler contains high priority code, so, for fairness, when the Delay completes all other callbacks that are already queued in the UI message loop should run before my handler resumes execution.
Perhaps in your case, you can indeed guarantee this. But for a complex GUI, this is difficult. Remember that all bindings, command states, and observable collections are also updated on the UI thread. So, guaranteeing the completion of all manually called methods isn't enough—the UI thread must be free of any work, and a lot can be happening there.
Thanks EldHasp for the results. I am surprised that the HighPrecisionTimer.Delay is more responsive/accurate than the DispatcherTimerDelay, when the "Dispatcher Invoke Count" is larger than 0. How do you explain that? Is it because of the default DispatcherPriority of the DispatcherTimer? If you instantiate the timer with DispatcherPriority.Normal or DispatcherPriority.Send does the difference go away?
Using DispatcherTimer timer = new(DispatcherPriority.Send); can reduce the latency somewhat, but it will still occur because use ui-thread contention remains, albeit reduced. It's also impossible to predict in advance whether another task will send a UI request to the thread with DispatcherPriority.Send priority. If so, the contention will further increase the latency.
I would also recommend creating a high-precision timer with 1 ms accuracy using the WinAPI functions timeSetEvent and timeKillEvent. This solution, in my opinion, is better than the one I've provided. The only drawback to using WinAPI is that it's limited to the Windows OS platform.
Yep. Emperor Eto also mentioned multimedia timers in a comment under the question, but let's not lose focus from what is asked in the question. It's not about millisecond accuracy. It's about comparing the Task.Delay with the DispatcherTimer, and listing the advantages/disadvantages of each approach for the purpose of suspending an async WPF event handler.
Comparing Task.Delay with DispatcherTimer requires a task context. Under certain conditions, one will be more accurate than the other. Both timers rely on a system timer, which typically operates with an accuracy of about 15 ms. The only difference between them is the thread on which the timer callback (or event) occurs. Pool threads have low priority, so even if they are overloaded with work, code on the main application thread can still execute. However, there is only one main application thread; it can perform many different tasks, and it can be overloaded with work very oft
It also matters which thread executes the code before and after the delay. Therefore: 1) If you don't have any special requirements, complex conditions, or the likelihood of high thread load, then use Task.Delay. 2) If you have high thread pool load, then use DispatcherTimer. 3) If you have high main thread load, then use Task.Delay. However, if the code after the Delay needs to be executed on the main thread, its execution may not begin immediately after the Delay terminates.
4) If there is a likelihood of high load on both the thread pool and the main thread, then use some custom implementations. My solution is not the only one that will work; many others are available. You can also use System.Timers.Timer with a higher priority for its thread.
"If you have high main thread load, then use Task.Delay." -- Why? What is the advantage of the Task.Delay over the DispatcherTimerDelay when the UI thread is overloaded? The code after the await Delay must run on the UI thread, that's a requirement of the question.
When using DispatcherTimerDelay, the code will execute in the following sequence: 1) All code until the task returns will be executed immediately; 2) After that, there will be a wait for the task to complete, which requires { timer.Stop(); tcs.SetResult(); } to be executed. When the timer fires, this code will be placed in the dispatcher queue. This will be the first wait in the queue;
3) When the task wait completes, the subsequent code, in this case tbDispatcherTimerDelay.Text = $"{interval:0}";, will also be placed in the dispatcher queue. This will be the second wait in the queue. Therefore, if the main thread is overloaded with work, using DispatcherTimerDelay will result in two waits in the busy thread's queue. If you use Task.Delay, the first wait will be on the thread pool queue, and the busy queue will only need one wait to execute the code after the wait.
Therefore, for the given conditions, using Task.Delay will be simpler and at least as good as using DispatcherTimerDelay.
|
0

Task.Delay deep under the hood uses native Thread Pool, so it operates on different threads than main UI thread. In case of thread pool saturation you have to wait until new thread will be spawned. When Task.Delay ends the execution have to return to the main thread with a small help from a Dispatcher and SynchronizationContext.

DispatcherTimer deep under the hood uses native Timers, so it operates only on the main thread, but have to allocate a kernel object - the Timer, and also have to release it when you call .Stop(). Returning to the main thread is not needed, because you're already there.

You may use await Task.Delay(...).ConfigureAwait(false) to escape from the main thread and execute continuation on the thread from the pool, like with Task.Run(...).

Do I get any benefit from using the MyDelay instead of the Task.Delay, like consuming less resources or being more responsive under load? Or eventually both approaches amount to the same thing?

Task.Delay uses less resources - thread pool is already allocated and ready to use. DispatcherTimer have to create a Timer and release it afterwards. In both methods you need some memory from the heap to create managed objects like Tasks and delegates. On the other hand DispatcherTimer has less things to do in terms of computing, so should be faster.

More specifically I know that the Task.Delay under the hood uses the ThreadPool and the System.Threading timer infrastructure. Does the DispatcherTimer uses the same infrastructure, or it's based on some other WPF-specific device?

DispatcherTimer uses very different infrastructure, but still it's a native windows infrastructure.

being more responsive under load...

Just do not do any time-consuming things on the main UI thread and do not use Thread.Sleep in threads from the thread pool. Everything should be fine then.

7 Comments

Thanks Sinus for the answer. Is there any way to verify experimentally that the Task.Delay uses less resources than the DispatcherTimer? The ​​GC.GetTotalAllocatedBytes method does not include any native allocations, so the native timer allocated by the DispatcherTimer will not appear in the measurements. Maybe the Process.WorkingSet64 could do the trick, after creating 1,000,000 of each type of timer?
You can't create 1,000,000 native timers. There is a limit 10,000 per process and 65,535 systemwide including resources and other internal types. Windows architecture reasons learn.microsoft.com/en-us/previous-versions/ms810501(v=msdn.10)
Btw AFAIK the Task.Delay doesn't just use the ThreadPool under the hood. The ThreadPool is used only for completing the Task. There is additional infrastructure for signaling the time that the Task should be completed. Look at the internal classes TimerQueue and TimerQueueTimer in the Timer.cs source code file. Lots of stuff is going on there.
That's true, there is a lots of stuff. You have only one instance of TimerQueue in the process, no matter how may times you call Task.Delay, so don't count it. TimerQueueTimer is also a single instance object. It even do all his work in thread from the ThredPool (this is why it implements IThreadPoolWorkItem). Don't count it also. Whole infrastructure is very performant so in practice do not bother when using it.
Unfortunately there's no good answer for the question "Should I use Task.Delay or DispatcherTimer in general?" Everything depends if you want to stick to the UI thread, or not. For a simple one-time delay Task.Delay should be better. For UI related staff and repeating operations like animations the DispatcherTimer will be better. In practice the cost of use any of them is completely insignificant.
Sinus my question is focused on suspending an async event handler on the UI thread, not on repeating operations. I wonder if there is any benefit to switching from the Task.Delay to a custom Delay based on the DispatcherTimer. I understand that the difference between them is most likely minuscule, but nevertheless I wanted to know if one is even marginally better than the other. It's mostly a curiosity-driven question, that might have some educational value for the readers and myself. In this context, saying "this or that is better" without justification doesn't provide much value.
For the question "Is there any benefit to switching from the Task.Delay to DispatcherTimer?" I would say: hardly any. You still have to allocate a Task and use await/async. In general none of this approaches is better than the other. One of this approaches may be better than the other depending on context and usage. Sorry, but this is one of those cases where a general, always valid answer cannot be given. And always remember that when you write await, you're not suspending the thread, but rather conditionally schedules continuation and returns from the method.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.