【问题标题】:Simple lockless stopwatch简单的无锁秒表
【发布时间】:2016-06-14 15:30:03
【问题描述】:

根据 MSDN,Stopwatch 类实例方法对于多线程访问是不安全的。这也可以通过检查各个方法来确认。

但是,由于我只需要在代码中的几个位置使用简单的“经过时间”的计时器,我想知道它是否仍然可以无锁地完成,使用类似的东西:

public class ElapsedTimer : IElapsedTimer
{
    /// Shared (static) stopwatch instance.
    static readonly Stopwatch _stopwatch = Stopwatch.StartNew();

    /// Stopwatch offset captured at last call to Reset
    long _lastResetTime;

    /// Each instance is immediately reset when created
    public ElapsedTimer()
    { 
        Reset();
    }

    /// Resets this instance.
    public void Reset()
    {
        Interlocked.Exchange(ref _lastResetTime, _stopwatch.ElapsedMilliseconds);
    }

    /// Seconds elapsed since last reset.
    public double SecondsElapsed
    {
        get
        {
             var resetTime = Interlocked.Read(ref _lastResetTime);
             return (_stopwatch.ElapsedMilliseconds - resetTime) / 1000.0;
        }
    }
}

由于_stopwatch.ElapsedMilliseconds 基本上是对QueryPerformanceCounter 的调用,我假设从多个线程调用它是安全的?与普通的Stopwatch 不同的是,这个类基本上一直在运行,所以我不需要像Stopwatch 那样保持任何附加状态(“运行”或“停止”)。

(更新)

在@Scott 在下面的答案中提出建议后,我意识到Stopwatch 提供了一个简单的静态GetTimestamp 方法,它返回原始QueryPerformanceCounter 滴答声。也就是说,代码可以修改成这样,是线程安全的:

public class ElapsedTimer : IElapsedTimer
{
    static double Frequency = (double)Stopwatch.Frequency;

    /// Stopwatch offset for last reset
    long _lastResetTime;

    public ElapsedTimer()
    { 
        Reset();
    }

    /// Resets this instance.
    public void Reset()
    {
        // must keep in mind that GetTimestamp ticks are NOT DateTime ticks
        // (i.e. they must be divided by Stopwatch.Frequency to get seconds,
        // and Stopwatch.Frequency is hw dependent)
        Interlocked.Exchange(ref _lastResetTime, Stopwatch.GetTimestamp());
    }

    /// Seconds elapsed since last reset
    public double SecondsElapsed
    {
        get
        { 
            var resetTime = Interlocked.Read(ref _lastResetTime);
            return (Stopwatch.GetTimestamp() - resetTime) / Frequency; 
        }
    }
}

澄清一下,这段代码的想法是:

  1. 拥有一种简单快速的方法来检查自某个操作/事件以来是否已过时间,
  2. 如果从多个线程调用方法,则不应破坏状态,
  3. 必须不受操作系统时钟更改(用户更改、NTP 同步、时区等)的影响

我会像这样使用它:

private readonly ElapsedTimer _lastCommandReceiveTime = new ElapsedTimer();

// can be invoked by multiple threads (usually threadpool)
void Port_CommandReceived(Cmd command)
{
    _lastCommandReceiveTime.Reset();
}

// also can be run from multiple threads
void DoStuff()
{
    if (_lastCommandReceiveTime.SecondsElapsed > 10)
    {
        // must do something
    }
}

【问题讨论】:

  • Interlocked.ExchangeInterlocked.Read 是我相信的锁定机制
  • @justin.m.chase:不,它们都是无锁的(同时确保原子性)。在 x64 平台上,它们甚至会被 JITted 到实际的 CPU 指令。
  • 如果您要走那么远,为什么不自己调用 QueryPerformanceCounter 呢?
  • @ScottChamberlain:当然,我也可以这样做,我只是想检查一下我在这种方法中是否遗漏了什么?无论如何,这会更好,因为 Stopwatch 实现不必在未来的 .NET 版本中保持固定。唯一的好处是它在内部将滴答声转换为毫秒,所以我不必检查TickFrequency
  • @justin.m.chase:换句话说,当没有竞争时它快 2 倍,当两个线程竞争时快 100 倍。对于多线程,性能差异会更大。

标签: c# stopwatch atomic lockless


【解决方案1】:

我建议的唯一更改是使用 Interlocked.Exchange(ref _lastResetTime, _stopwatch.ElapsedTicks); 而不是毫秒,因为如果您处于高性能模式,则可以从 QueryPerformanceCounter 获得亚毫秒级的结果。

【讨论】:

  • +1 谢谢!不过有一个更正:我相信 Stopwatch 滴答实际上是 QueryPerformanceCounter 返回的原始滴答,而不是 DateTime 使用的 100ns 滴答。
【解决方案2】:

我建议创建Stopwatch 的多个实例,并且只在同一个线程上读取它。

我不知道您的异步代码是什么样的,但在伪代码中我会这样做:

Stopwatch watch = Stopwatch.Startnew();
DoAsyncWork((err, result) =>
{
  Console.WriteLine("Time Elapsed:" + (watch.ElapsedMilliseconds / 1000.0));
  // process results...
});

或者:

public DoAsyncWork(callback) // called asynchronously
{
  Stopwatch watch = Stopwatch.Startnew();
  // do work
  var time = watch.ElapsedMilliseconds / 1000.0;
  callback(null, new { time: time });
}

第一个示例假设 DoAsyncWork 工作在不同的线程中完成工作,然后在完成时调用回调,编组回调用者线程。

第二个例子假设调用者正在处理线程,这个函数自己完成所有的计时,将结果传回给调用者。

【讨论】:

  • 但重点是能够从任何线程重置看门狗,并安全地检查时间是否已过。此外,您的第一个 sn-p 捕获了 watch 变量,但没有采取任何措施来防止它在异步方法之外被访问。我使用的是staticStopwatch,因为我基本上只需要QueryPerformanceCounter 的静态p/invoke,因此无需为每个计时器实例分配其所有字段(只需要一个long) .
  • 那么你将不得不进行锁定。
猜你喜欢
  • 1970-01-01
  • 2018-02-17
  • 2015-01-01
  • 2020-07-13
  • 1970-01-01
  • 1970-01-01
  • 2015-12-08
  • 1970-01-01
  • 2014-04-10
相关资源
最近更新 更多