0

I have a function which needs to return the precise amount of time that passed since it was called last. Currently, it's implemented like this:

    public TimeSpan Span()
    {
        lock (this)
        {
            var now = DateTime.UtcNow;
            var r = now - lastCallTime;
            lastCallTime = now;
            return r;
        }
    }

My issue with this method is that it uses lock, which can have significant performance impact.

Is a way of implementing this without using locks altogether?

Arsen Zahray
  • 24,367
  • 48
  • 131
  • 224
  • `DateTime` is only accurate to around 50ms, so that's not going to be good enough anyway. You should be using `Stopwatch`. – Matthew Watson Sep 12 '18 at 12:36
  • @MatthewWatson noted, thanks! – Arsen Zahray Sep 12 '18 at 12:37
  • @mjwills I'm using this combined with randomization to make software perform different actions with set time frequencies. Implementing it like this allows me not to store state information. The less precise the interval, the more what the software does will deviate from what I tell it to do – Arsen Zahray Sep 12 '18 at 12:40
  • How often will `lock` face contention? 1% of the time? 70%? – mjwills Sep 12 '18 at 12:41
  • @mjwills this is a part of a library I'm going to be re-using over and over again. If there is a way to implement this without locking, I'd like to do that – Arsen Zahray Sep 12 '18 at 12:44

2 Answers2

2

I would recommend using:

public long lastTimestamp = Stopwatch.GetTimestamp();

public TimeSpan Span()
{
    do
    {
        long oldValue = lastTimestamp;
        long currentTimestamp = Stopwatch.GetTimestamp();

        var previous = Interlocked.CompareExchange(ref lastTimestamp, currentTimestamp, oldValue);

        if (previous == oldValue)
        {
            // We effectively 'got the lock'
            var ticks = (currentTimestamp - oldValue) * 10_000_000 / Stopwatch.Frequency;
            return new TimeSpan(ticks);
        }
    } while (true);

    // Will never reach here
    // return new TimeSpan(0);
}

This will be thread-safe without the need for an explicit lock. And if there is contention on lastTimestamp then the code will loop until it works. This does mean that multiple calls to Span may not 'finish' in the same order that they 'started'.

A simpler approach to consider (but see the caveat below) would be:

public long lastTimestamp = Stopwatch.GetTimestamp();

public TimeSpan Span()
{
    long currentTimestamp = Stopwatch.GetTimestamp();

    var previous = Interlocked.Exchange(ref lastTimestamp, currentTimestamp);

    var ticks = (currentTimestamp - previous) * 10_000_000 / Stopwatch.Frequency;

    return new TimeSpan(ticks);
}

This will be thread-safe without the need for an explicit lock. Interlocked.Exchange generally outperforms lock.

As per the docs, Interlocked.Exchange:

Sets a 64-bit signed integer to a specified value and returns the original value, as an atomic operation.

This code is simpler, but due to the way that Interlocked.Exchange works (see Matthew Watson's great answer), the TimeSpan returned might be negative in high contention scenarios. This will not occur with the first solution, but the first solution will be slower with high contention.

mjwills
  • 23,389
  • 6
  • 40
  • 63
2

Just for completeness, I want to show you how the simpler code (second solution) from the accepted answer could return a negative value, as noted in that answer.

This thought-experiment uses two threads, designated T1 and T2. I'm prefixing the stack variables with T1 and T2 so you can tell them apart (below).

Let's assume that lastTimeStamp starts at 900 and the current time is 1000.

Now consider the following interlaced thread operations:

T1: long currentTimestamp = Stopwatch.GetTimestamp(); 
    => T1:currentTimeStamp = 1000
T2: long currentTimestamp = Stopwatch.GetTimestamp(); 
    => T2:currentTimeStamp = 1010
T2: var previous = Interlocked.Exchange(ref lastTimestamp, T2:currentTimestamp);
    => T2:previous = 900, lastTimestamp = 1010
T1: var previous = Interlocked.Exchange(ref lastTimestamp, T1:currentTimestamp);
    => T1:previous = 1010, lastTimestamp = 1000
T1: var ticks = (T1:currentTimestamp - T1:previous)
    => ticks = 1000 - 1010 = -10
T2: var ticks = (T2:currentTimestamp - T2:previous)
    => ticks = 1010 - 900 = 110

As you can see, thread T1 will end up returning -10.


[Addendum]

Here's my take on it - I'm not bothering to convert the stopwatch timestamp to a TimeSpan; I'm just leaving it in the units returned from Stopwatch.GetTimestamp() for brevity (and it will be slightly quicker):

public static long Span()
{
    long previous;
    long current;

    do
    {
        previous = lastTimestamp;
        current = Stopwatch.GetTimestamp();
    }
    while (previous != Interlocked.CompareExchange(ref lastTimestamp, current, previous));

    return current - previous;
}

static long lastTimestamp = Stopwatch.GetTimestamp();

This is the same solution as the accepted answer above, just organised slightly differently.

mjwills
  • 23,389
  • 6
  • 40
  • 63
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276