Thread safe DateTime update using Interlocked.*
Asked Answered
C

5

22

Can I use an Interlocked.* synchronization method to update a DateTime variable?

I wish to maintain a last-touch time stamp in memory. Multiple http threads will update the last touch DateTime variable.

I appreciate that DateTime variables are value types that are replaced rather than updated.

The best I can come up with is to hold the timestamp as total ticks in a long

class x
{
  long _lastHit;

  void Touch()
  {
    Interlocked.Exchange(ref _lastHit, DateTime.Now.Ticks);
  }
}
Congest answered 7/10, 2009 at 13:31 Comment(1)
i know this is an old question but considering that you can't guarantee the order of execution of instructions in the CPU how do you know that you are not replacing an a slightly newer timestamp with an older one ? (we're talking nano-seconds here though)Arana
S
4

Yes, you can do this. Your biggest problem may be that DateTime.Ticks only has a resolution of ~20 ms. So it doesn't really matter if you keep a DateTime last or a long ticks variable. But since there is no overload of Exchange for DateTime, you need to use long.

Seawright answered 7/10, 2009 at 13:43 Comment(5)
@Adam - As I was saying before your answer was removed. Thanks for mentioning the volatile keyword it was a gap in my knowledge. However from the docs it seems volatile only provides threadsafe reads but not updates.Congest
@Henk - 20ms is ok in this case. The use-case for the touch timestamp is a multi tenant web system. At 2am in the morning I need to check all users hosted on a DB instance are out of the system before running a data fix script.Congest
If you want more accuracy with your last update, you can use the System.Diagnostics.Stopwatch.GetTimestamp method msdn.microsoft.com/en-us/library/…Corsetti
Or you could use another property of DateTime... see my answer.Gynous
Correction: DateTime.Ticks can store time in 100ns increments (its pretty close to a 64-bit counter). However, if you read from DateTime.Now, it increments in jumps of 20ms, which means its far less accurate compared to Stopwatch class (however, the Stopwatch class drifts a few seconds every hour or so, which means it is less accurate when measuring longer time periods - thats another story).Malaya
G
21

Option 1: use a long with Interlocked and DateTime.ToBinary(). This doesn't need volatile (in fact you'd get a warning if you had it) because Interlocked already ensures an atomic update. You get the exact value of the DateTime this way.

long _lastHit;

void Touch()
{
    Interlocked.Exchange(ref _lastHit, DateTime.Now.ToBinary());
}

To read this atomically:

DateTime GetLastHit()
{
    long lastHit = Interlocked.CompareExchange(ref _lastHit, 0, 0);
    return DateTime.FromBinary(lastHit);
}

This returns the value of _lastHit, and if it was 0 swaps it with 0 (i.e. does nothing other than read the value atomically).

Simply reading is no good - at a minimum because the variable isn't marked as volatile, so subsequent reads may just reuse a cached value. Combining volatile & Interlocked would possibly work here (I'm not entirely sure, but I think an interlocked write cannot be seen in an inconsistent state even by another core doing a non-interlocked read). But if you do this you'll get a warning and a code smell for combining two different techniques.

Option 2: use a lock. Less desirable in this situation because the Interlocked approach is more performant in this case. But you can store the correct type, and it's marginally clearer:

DateTime _lastHit;
object _lock = new object();

void Touch()
{
    lock (_lock)
        _lastHit = DateTime.Now;
}

You must use a lock to read this value too! Incidentally, besides mutual exclusion a lock also ensures that cached values can't be seen and reads/writes can't be reordered.

Non-option: do nothing (just write the value), whether you mark it as volatile or not. This is wrong - even if you never read the value, your writes on a 32 bit machine may interleave in such an unlucky way that you get a corrupted value:

Thread1: writes dword 1 of value 1
Thread2: writes dword 1 of value 2
Thread2: writes dword 2 of value 2
Thread1: writes dword 2 of value 1

Result: dword 1 is for value 2, while dword 2 is for value 1
Gynous answered 26/1, 2010 at 15:42 Comment(7)
I did the math to consider the impacts of Interlocked or not on DateTime and came up with some useful figures. 1 second is 10 million ticks, putting the 32-bit boundary at 429.5 seconds or 7.166 minutes. That means non-Interlocked writes could result in a timestamp that's 7 minutes too low OR too high, every 7 minutes. Interlocked writes with non-Interlocked reads could ONLY result in a timestamp that's 7 minutes too high, because the first half is written first - roughly: value is 00019999, thread1 goes to write 00020000, thread2 sees 0002999, thread1 finishes with 00020000.Peba
Isn't it a bad idea to lock on an object without strong identity such as the default Object implementation?Electrobiology
@NormanH not at all. It's the best idea to lock on an object that's definitely not locked by any other code unknowingly. A private new object() is by far the easiest way of guaranteeing this. Other than that, it makes absolutely no difference what the type of the instance is; the locks are the same.Gynous
Only reason I commented was that I had seen a comment from Telerik's JustCode tool about locking on an object without strong identity. I did run across this SO question that seems like a good read about locking though - https://mcmap.net/q/588889/-locking-on-an-objectElectrobiology
Quite old now, but worth pointing out to anyone reading @NormanH's comment that a private object does have strong identity. "Weak identity" means that another piece of code elsewhere could access the same instance, especially if code in another app domain could do so (e.g. this can happen with interned strings).Alphabetic
Why not just use Interlocked.Read instead of Interlocked.CompareExchange?Enriqueenriqueta
So... is it wrong on one of those increasingly common 64-bit machines?Settle
S
4

Yes, you can do this. Your biggest problem may be that DateTime.Ticks only has a resolution of ~20 ms. So it doesn't really matter if you keep a DateTime last or a long ticks variable. But since there is no overload of Exchange for DateTime, you need to use long.

Seawright answered 7/10, 2009 at 13:43 Comment(5)
@Adam - As I was saying before your answer was removed. Thanks for mentioning the volatile keyword it was a gap in my knowledge. However from the docs it seems volatile only provides threadsafe reads but not updates.Congest
@Henk - 20ms is ok in this case. The use-case for the touch timestamp is a multi tenant web system. At 2am in the morning I need to check all users hosted on a DB instance are out of the system before running a data fix script.Congest
If you want more accuracy with your last update, you can use the System.Diagnostics.Stopwatch.GetTimestamp method msdn.microsoft.com/en-us/library/…Corsetti
Or you could use another property of DateTime... see my answer.Gynous
Correction: DateTime.Ticks can store time in 100ns increments (its pretty close to a 64-bit counter). However, if you read from DateTime.Now, it increments in jumps of 20ms, which means its far less accurate compared to Stopwatch class (however, the Stopwatch class drifts a few seconds every hour or so, which means it is less accurate when measuring longer time periods - thats another story).Malaya
C
2

With the new Unsafe class it is finally possible directly on a DateTime field:

public static DateTime InterlockedCompareExchange(ref DateTime location1, DateTime value, DateTime comparand)
{
    var storedAsLong = Interlocked.CompareExchange(ref Unsafe.As<DateTime, long>(ref location1),
                                                   value: Unsafe.As<DateTime, long>(ref value),
                                                   comparand: Unsafe.As<DateTime, long>(ref comparand));

    return Unsafe.As<long, DateTime>(ref storedAsLong);
}

REMARK (WARNING): the equality here is different from the regular one of the DateTime:

  • Regular DateTime Equality: (DateTime x, DateTime y) => x.Ticks == y.Ticks;
  • Equality Here: (DateTime x, DateTime y) => x.Ticks == y.Ticks && x.Kind == y.Kind;

Equality here compares all bits of the DateTime, regular one omits bits dedicated to the Kind.

Chantel answered 12/1, 2023 at 11:18 Comment(0)
E
0

EDIT: based on comments below from @romkyns [Thanks]

If your code is running on a 32 bit machine. then a 64 bit long is going to be written to memory in two atomic operations, which can be interrupted by a context switch. So in general you do need to deal with this issue.

But to be clear, for this specific scenario, (writing a long value which represents time ticks) it could be argued that the problem is so very unlilely as to be not worth dealing with... since (except for a split second once every 2^32 ticks), the value in the high word (32 bits) will be the same for any two concurrent writes anyway... and even in the very unlikely event that there are two concurrent writes which span that boundary, which concurrently interrupt each other and you get the hi word from one and the low word from the other, unless you are also reading this value every millesecond, the next write will fix the issue anyway, and no harm would be done. Taking this approach, however, no matter how unlikely the bad case might be, still allows for the extremely slim but possible scenario of gettign a wrong value in there once in every 4 Billion ticks... (And good luck trying to reproduce that bug...)

If you are running on a 64 bit machine, otoh, (much more likely at this point in time but not guaranteed) then the value in the 64 bit memory slot is written atomically, and you don't need to worry about concurrency here. A race condition (Which is what you are trying to prevent) can only occur if there is some program invariant that is in an invalid state during some block of processing that can be interrupted by another thread. If all you are doing is writing to the lastTouch DateTime variable (memory location) then there is no such invlaid invariant to be concerned with, and therefore you do not need to worry about concurrent access.

Essary answered 7/10, 2009 at 14:2 Comment(7)
You are right about concurrency and logic but writing a long is not atomic so you do need Lock or Interlocked.Seawright
@Charles - As Henk says I am concerned about reading a partially updated long value. A highly unlikely situation I appreciate.Congest
camelCase, "highly unlikely" just means harder to find & debugSeawright
But the CLR Specification states "A conforming CLI shall guarantee that read and write access to properly aligned memory locations no larger than the native word size is atomic when all the write accesses to a location are the same size." So unless you're still on a 32 bit machine, running 32 bit Windows, writing to a long IS atomic, no ? ---- Granted, if you aren't sure that your app will always be executed in a 64 bit hardware/OS environment, I understand taking the caustious approachEssary
I think you should make it VERY clear that your answer only holds if nothing ever reads the value AND that you're positive you're on a native 64-bit platform. Otherwise it's highly misleading. I seriously doubt you can assume native 64-bitness for at least another 10 years, likely more (think of all those netbooks...)Gynous
Don't be so dismissive! "the next write will fix the issue anyway, and no harm would be done..." only if there IS a next write. It is possible in some apps that bugs caused by the incorrect value will ensure there will never be a next write, or it will just be too late. Which might be 'harm done' in any time critical scenario e.g. Therac-25. T.TSettle
@Tim Lovell-Smith, Wow, I answered this eleven years ago... I had to research it again to refresh my memory. You are absolutely correct, but I discuss this in my answer. There is a very slight but positive non-zero chance that this would cause an issue. Note: there would still need to be an invariant whose incorrect value causes a mistake in program outcome.Essary
G
0

My approach is not one of the best, but you can use a string var to store a formatted date and then parse it back into datetime:

class x
{
  string _lastHit;

  void Touch()
  {
    Interlocked.Exchange( ref _lastHit, DateTime.Now.ToString("format your date") );   
  }
}

When you need to use this value, just parse into DateTime:

DateTime.Parse(_lastHit)

The parsing is always working, because the string is formatted using the DateTime class, but you can use TryParse to handle possible parsing errors

Gaudy answered 11/10, 2017 at 9:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.