C#中的await Task.Delay(1000)仅需640毫秒返回。

11

所以,这很容易解释,但是我已经搜索了一下,没有找到有相同问题的人。我的问题源于一个长期定期任务,它比我想要的频率更频繁地运行。似乎我创建和等待的每个Task.Delay()都会在指定的延迟时间的约65%处返回。

问题归结为以下代码行大约在640-660ms内返回(根据Visual Studio的显示。我在这行代码和随后的一行代码上设置了断点,并且它说已经过去了那么长时间):

await Task.Delay(1000); 

在另外两台机器上,完全相同的代码库都可以正常运行。不仅上述简单语句如此,周期性任务也是如此。是否有某处设置会影响Task.Delay(int millisecondsDelay)?时钟类型、时钟速度、任何东西、系统时钟?我很迷惑...

编辑:

在下面的片段中,EtMilliseconds的值在130-140毫秒之间,大约是上面期望持续时间的65%。除了第一次进入while()外,从未超出这个范围。

Long EtMilliseconds;
Stopwatch etWatch = new Stopwatch();
etWatch.Restart();

while (true)
{
  EtMilliseconds = etWatch.ElapsedMilliseconds;
  taskDelay = Task.Delay(200);
  etWatch.Restart();
  await taskDelay;
}

编辑2:

以下代码导致 EtMilliseconds 再次变为约131毫秒。使用 Thread.Sleep 似乎没有效果...

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void button_Click(object sender, RoutedEventArgs e)
    {
        long EtMilliseconds;
        Stopwatch etWatch = new Stopwatch();
        etWatch.Restart();

        while (true)
        {
            EtMilliseconds = etWatch.ElapsedMilliseconds;
            label.Content = EtMilliseconds.ToString();
            etWatch.Restart();
            Thread.Sleep(200);
        }
    }
}
这个片段是相同的,但使用了Task.Delay(200)。这个会正确地更新GUI标签(Thread.Sleep不行),时间要么是131ms,要么是140ms。始终如此...
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private async void button_Click(object sender, RoutedEventArgs e)
    {
        Task taskDelay;
        long EtMilliseconds;
        Stopwatch etWatch = new Stopwatch();
        etWatch.Restart();

        while (true)
        {
            EtMilliseconds = etWatch.ElapsedMilliseconds;
            label.Content = EtMilliseconds.ToString();
            taskDelay = Task.Delay(200);
            etWatch.Restart();
            await taskDelay;
        }

    }
}

编辑3:

使用DispatcherTimer,我仍然从我的Stopwatch.ElapsedMilliseconds得到大约130ms...但是有一件奇怪的事情。如果我还更新了DateTime.Now()的显示,它们会增加大约200ms(或略多),这正是我所期望的。这是什么鬼?!? enter image description here

public partial class MainWindow : Window
{

    public long etMilliseconds;
    public Stopwatch etWatch;

    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this;
    }

    //  System.Windows.Threading.DispatcherTimer.Tick handler
    //
    //  Updates the current seconds display and calls
    //  InvalidateRequerySuggested on the CommandManager to force 
    //  the Command to raise the CanExecuteChanged event.
    private void dispatcherTimer_Tick(object sender, EventArgs e)
    {
        // Updating the Label which displays the current second
        tBoxCurrTime.Text += DateTime.Now.ToString("yyyy_MMM_dd-hh:mm:ss.fff_tt") + "\n";
        tBoxMilliSecElapsed.Text += etWatch.ElapsedMilliseconds + "\n";
        etWatch.Restart();

        // Forcing the CommandManager to raise the RequerySuggested event
        CommandManager.InvalidateRequerySuggested();
    }

    private void button_Click(object sender, RoutedEventArgs e)
    {
        etWatch = new Stopwatch();

        //  DispatcherTimer setup
        DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
        dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
        dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 200);
        dispatcherTimer.Start();

        etWatch.Restart();
    }
}

这是一个异步调用,它不应该持续1000毫秒。相反,当1000毫秒过去时,它将从下一行继续执行。你确定你在那里正确地测量时间吗?你能编辑你的问题展示如何进行测量吗? - Wiktor Zychla
1
你用什么来测量 Task.Delay 开始和结束之间的时间?如果你想要准确,应该使用一个 Stopwatch 实例。同时确保 .Delay 被正确地等待,并且调用栈一直使用 async/await 模式。还要确保你的 StopWatch 的作用域被正确设置,你不希望另一个线程可以更改共享实例。 - Igor
1
@Igor 我认为他正在使用调试器内置的工具,该工具显示执行“跨步”操作所需的时间。但我同意,尝试使用StopWatch进行测量。 - Scott Chamberlain
1
@GaryWillette 你试过使用 Thread.Sleep 吗?只是为了尝试一下。我肯定在我的机器上无法复现 :-/ - Jcl
@devuxer 是的,那似乎是正确的。计时器和VisualStudio调试器都一致且不正确。DateTime.Now()和真实时间是准确的。这已经通过您建议的10秒延迟进行了确认。 - Gary Willette
显示剩余28条评论
2个回答

2

根据目前的实验结果,我尝试给出一个猜测性答案。

Stopwatch 类使用 Windows “性能计数器”。我经常听说在某些系统上它会返回不准确的数据。这似乎发生在旧硬件和/或旧操作系统版本上。

例如,时间会根据你所执行的核心而跳动。这可能不是您的情况,因为您的计时始终不准确。但这是这种时间数据的问题示例。

我猜 Visual Studio 也使用了 Stopwatch

这也符合问题只发生在您的计算机上的事实。其他计算机可能有不同的时间硬件。

请尝试以下操作:

var sw = Stopwatch.StartNew();
var startDateTime = DateTime.UtcNow;

Thread.Sleep(200);

sw.Stop();
var endDateTime = DateTime.UtcNow;

并发布结果。我的预测是Stopwatch版本是错误的,而基于DateTime的版本显示略高于200毫秒。
据我所知,Windows内核使用DateTime.UtcNow使用的时间源来进行自己的计时器和延迟。 AFAIK这是一个硬件中断,默认情况下以60Hz的速度滴答并导致计时器以该速率更新全局时间变量。 这意味着即使DateTime.UtcNow是错误的,它也应与Thread.Sleep一致。 但我们知道DateTime.UtcNow是正确的。 否则,您会注意到显着的系统时间漂移。
也许您可以尝试禁用提供Windows高频计数器的硬件部件。 Stopwatch.IsHighResolution现在应返回true,并且在禁用此硬件部件时应变为false。 在我的机器上,它在设备管理器中称为“HPET”。

在我的最后一次编辑中(#3),我准确地展示了...我的DateTime.Now()每次递增200毫秒或更多,而我的etWatch.ElapsedMilliseconds显示大约130-140毫秒。 - Gary Willette
@usr 我在devmgr中禁用了高性能事件定时器,但当我运行代码时,Stopwatch.IsHighResolution仍然为true。我需要重新启动吗?在devmgr中显示它已被禁用... - Gary Willette
我至少会重新启动Visual Studio。 - devuxer
我相信应用程序不希望在其下时间源发生变化。重新启动。 - usr
显示剩余10条评论

0

为了结束这个话题,如果有其他人遇到类似的问题,我已经找到了解决方法。大约一个月前,我的Windows 7的'Aero'效果开始变得非常缓慢。在寻找解决方案时,我发现了以下内容:

http://www.tomshardware.com/forum/57307-63-slow-motion-aero-transitions

我使用的解决方案在上面的链接中,具体步骤如下:
以管理员身份运行CMD。输入"bcdedit /set useplatformclock true"(不带引号),然后按回车键。重新启动计算机,问题就得到了解决。
我并不完全理解这个命令的内部工作原理,如果有人想要详细说明,请随意。但我知道,它不仅解决了我的慢速Aero效果,还让我的秒表计时器准确无误!
感谢大家的努力,我非常感激!
Gary

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接